Merge pull request #127 from glower/hipchat-approve-bot-wip

Hipchat approve bot
pull/128/head
Karolis Rusenas 2017-12-23 10:11:44 +00:00 committed by GitHub
commit 44c17693a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 142 additions and 215 deletions

42
bot/approvals.go Normal file
View File

@ -0,0 +1,42 @@
package bot
import (
"bytes"
"fmt"
"github.com/keel-hq/keel/approvals"
"github.com/keel-hq/keel/bot/formatter"
)
func ApprovalsResponse(approvalsManager approvals.Manager) string {
approvals, err := approvalsManager.List()
if err != nil {
return fmt.Sprintf("got error while fetching approvals: %s", err)
}
if len(approvals) == 0 {
return fmt.Sprintf("there are currently no request waiting to be approved.")
}
buf := &bytes.Buffer{}
approvalCtx := formatter.Context{
Output: buf,
Format: formatter.NewApprovalsFormat(formatter.TableFormatKey, false),
}
err = formatter.ApprovalWrite(approvalCtx, approvals)
if err != nil {
return fmt.Sprintf("got error while formatting approvals: %s", err)
}
return buf.String()
}
func RemoveApprovalHandler(identifier string, approvalsManager approvals.Manager) string {
err := approvalsManager.Delete(identifier)
if err != nil {
return fmt.Sprintf("failed to remove '%s' approval: %s.", identifier, err)
}
return fmt.Sprintf("approval '%s' removed.", identifier)
}

View File

@ -93,7 +93,6 @@ func Run(k8sImplementer kubernetes.Implementer, approvalsManager approvals.Manag
"error": err,
}).Fatalf("main: failed to setup %s bot\n", botName)
} else {
log.Debugf(">>> Run [%s] bot", botName)
teardowns[botName] = teardownBot
}
}
@ -101,7 +100,7 @@ func Run(k8sImplementer kubernetes.Implementer, approvalsManager approvals.Manag
func Stop() {
for botName, teardown := range teardowns {
log.Infof("Teardown %s bot\n", botName)
log.Infof("Teardown %s bot", botName)
teardown()
}
}

View File

@ -1,10 +1,11 @@
package slack
package bot
import (
"bytes"
"fmt"
"github.com/keel-hq/keel/bot/formatter"
"github.com/keel-hq/keel/provider/kubernetes"
"k8s.io/client-go/pkg/apis/extensions/v1beta1"
@ -18,16 +19,16 @@ type Filter struct {
}
// deployments - gets all deployments
func (b *Bot) deployments() ([]v1beta1.Deployment, error) {
func deployments(k8sImplementer kubernetes.Implementer) ([]v1beta1.Deployment, error) {
deploymentLists := []*v1beta1.DeploymentList{}
n, err := b.k8sImplementer.Namespaces()
n, err := k8sImplementer.Namespaces()
if err != nil {
return nil, err
}
for _, n := range n.Items {
l, err := b.k8sImplementer.Deployments(n.GetName())
l, err := k8sImplementer.Deployments(n.GetName())
if err != nil {
log.WithFields(log.Fields{
"error": err,
@ -49,8 +50,8 @@ func (b *Bot) deployments() ([]v1beta1.Deployment, error) {
return impacted, nil
}
func (b *Bot) deploymentsResponse(filter Filter) string {
deps, err := b.deployments()
func DeploymentsResponse(filter Filter, k8sImplementer kubernetes.Implementer) string {
deps, err := deployments(k8sImplementer)
if err != nil {
return fmt.Sprintf("got error while fetching deployments: %s", err)
}

View File

@ -1,23 +1,19 @@
package hipchat
import (
"bytes"
"fmt"
"strings"
"github.com/keel-hq/keel/bot"
"github.com/keel-hq/keel/bot/formatter"
"github.com/keel-hq/keel/types"
log "github.com/Sirupsen/logrus"
)
func (b *Bot) subscribeForApprovals() error {
log.Debugf(">>> hipchat.subscribeForApprovals()\n")
approvalsCh, err := b.approvalsManager.Subscribe(b.ctx)
if err != nil {
log.Debugf(">>> [ERROR] hipchat.subscribeForApprovals(): %s\n", err.Error())
log.Errorf("hipchat.subscribeForApprovals(): %s", err.Error())
return err
}
@ -39,18 +35,11 @@ func (b *Bot) subscribeForApprovals() error {
// Request - request approval
func (b *Bot) requestApproval(req *types.Approval) error {
msg := fmt.Sprintf(`Approval required!
%s
To vote for change type '%s approve %s'
To reject it: '%s reject %s'
Votes: %d/%d
Delta: %s
Identifier: %s
Provider: %s`,
msg := fmt.Sprintf(ApprovalRequiredTempl,
req.Message, b.mentionName, req.Identifier, b.mentionName, req.Identifier,
req.VotesReceived, req.VotesRequired, req.Delta(), req.Identifier,
req.Provider.String())
return b.postMessage(msg)
return b.postMessage(formatAsSnippet(msg))
}
func (b *Bot) processApprovalResponses() error {
@ -75,7 +64,6 @@ func (b *Bot) processApprovalResponses() error {
}).Error("bot.processApprovalResponses: failed to process approval reject response message")
}
}
}
}
}
@ -107,7 +95,6 @@ func (b *Bot) processApprovedResponse(approvalResponse *bot.ApprovalResponse) er
"identifier": identifier,
}).Error("bot.processApprovedResponse: got error while replying after processing approved approval")
}
}
return nil
}
@ -136,7 +123,6 @@ func (b *Bot) processRejectedResponse(approvalResponse *bot.ApprovalResponse) er
"identifier": identifier,
}).Error("bot.processApprovedResponse: got error while replying after processing rejected approval")
}
}
return nil
}
@ -144,124 +130,18 @@ func (b *Bot) processRejectedResponse(approvalResponse *bot.ApprovalResponse) er
func (b *Bot) replyToApproval(approval *types.Approval) error {
switch approval.Status() {
case types.ApprovalStatusPending:
b.postMessage("Vote received")
// "Vote received",
// "All approvals received, thanks for voting!",
// types.LevelInfo.Color(),
// []slack.AttachmentField{
// slack.AttachmentField{
// Title: "vote received!",
// Value: "Waiting for remaining votes.",
// Short: false,
// },
// slack.AttachmentField{
// Title: "Votes",
// Value: fmt.Sprintf("%d/%d", approval.VotesReceived, approval.VotesRequired),
// Short: true,
// },
// slack.AttachmentField{
// Title: "Delta",
// Value: approval.Delta(),
// Short: true,
// },
// slack.AttachmentField{
// Title: "Identifier",
// Value: approval.Identifier,
// Short: true,
// },
// })
msg := fmt.Sprintf(VoteReceivedTempl,
approval.VotesReceived, approval.VotesRequired, approval.Delta(), approval.Identifier)
b.postMessage(formatAsSnippet(msg))
case types.ApprovalStatusRejected:
b.postMessage("Change rejected")
// "Change rejected",
// "Change was rejected",
// types.LevelWarn.Color(),
// []slack.AttachmentField{
// slack.AttachmentField{
// Title: "change rejected",
// Value: "Change was rejected.",
// Short: false,
// },
// slack.AttachmentField{
// Title: "Status",
// Value: approval.Status().String(),
// Short: true,
// },
// slack.AttachmentField{
// Title: "Votes",
// Value: fmt.Sprintf("%d/%d", approval.VotesReceived, approval.VotesRequired),
// Short: true,
// },
// slack.AttachmentField{
// Title: "Delta",
// Value: approval.Delta(),
// Short: true,
// },
// slack.AttachmentField{
// Title: "Identifier",
// Value: approval.Identifier,
// Short: true,
// },
// })
msg := fmt.Sprintf(ChangeRejectedTempl,
approval.Status().String(), approval.VotesReceived, approval.VotesRequired,
approval.Delta(), approval.Identifier)
b.postMessage(formatAsSnippet(msg))
case types.ApprovalStatusApproved:
b.postMessage("approval received")
// "approval received",
// "All approvals received, thanks for voting!",
// types.LevelSuccess.Color(),
// []slack.AttachmentField{
// slack.AttachmentField{
// Title: "update approved!",
// Value: "All approvals received, thanks for voting!",
// Short: false,
// },
// slack.AttachmentField{
// Title: "Votes",
// Value: fmt.Sprintf("%d/%d", approval.VotesReceived, approval.VotesRequired),
// Short: true,
// },
// slack.AttachmentField{
// Title: "Delta",
// Value: approval.Delta(),
// Short: true,
// },
// slack.AttachmentField{
// Title: "Identifier",
// Value: approval.Identifier,
// Short: true,
// },
// })
msg := fmt.Sprintf(UpdateApprovedTempl,
approval.VotesReceived, approval.VotesRequired, approval.Delta(), approval.Identifier)
b.postMessage(formatAsSnippet(msg))
}
return nil
}
func (b *Bot) approvalsResponse() string {
approvals, err := b.approvalsManager.List()
if err != nil {
return fmt.Sprintf("got error while fetching approvals: %s", err)
}
if len(approvals) == 0 {
return fmt.Sprintf("there are currently no request waiting to be approved.")
}
buf := &bytes.Buffer{}
approvalCtx := formatter.Context{
Output: buf,
Format: formatter.NewApprovalsFormat(formatter.TableFormatKey, false),
}
err = formatter.ApprovalWrite(approvalCtx, approvals)
if err != nil {
return fmt.Sprintf("got error while formatting approvals: %s", err)
}
return buf.String()
}
func (b *Bot) removeApprovalHandler(identifier string) string {
err := b.approvalsManager.Delete(identifier)
if err != nil {
return fmt.Sprintf("failed to remove '%s' approval: %s.", identifier, err)
}
return fmt.Sprintf("approval '%s' removed.", identifier)
}

View File

@ -3,7 +3,6 @@ package hipchat
import (
"context"
"errors"
"fmt"
"os"
"regexp"
"strings"
@ -50,14 +49,16 @@ func init() {
// Run ...
func Run(k8sImplementer kubernetes.Implementer, approvalsManager approvals.Manager) (teardown func(), err error) {
if os.Getenv(constants.EnvHipchatApprovalsPasswort) != "" {
if os.Getenv(constants.EnvHipchatApprovalsPasswort) != "" &&
os.Getenv(constants.EnvHipchatApprovalsUserName) != "" {
botName := "keel"
if os.Getenv(constants.EnvHipchatApprovalsBotName) != "" {
botName = os.Getenv(constants.EnvHipchatApprovalsBotName)
}
botUserName := ""
if os.Getenv(constants.EnvHipchatApprovalsUserName) != "" { // need this!!!!
if os.Getenv(constants.EnvHipchatApprovalsUserName) != "" {
botUserName = os.Getenv(constants.EnvHipchatApprovalsUserName)
}
@ -85,22 +86,20 @@ func Run(k8sImplementer kubernetes.Implementer, approvalsManager approvals.Manag
return teardown, nil
}
log.Info("bot.hipchat.Run(): HipChat approval bot ist not configured, ignore")
return func() {}, nil
}
//--------------------- <XMPP client> -------------------------------------
func connect(username, password string) *hipchat.Client {
log.Debugf(">>> bot.hipchat.NewClient(): user=%s, pass=%s\n", username, password)
attempts := 10
for {
log.Debugf(">>> try to connect to hipchat")
log.Info("bot.hipchat.connect: try to connect to hipchat")
client, err := hipchat.NewClient(username, password, "bot", "plain")
// could not authenticate
if err != nil {
log.Errorf("bot.hipchat.connect: Error=%s\n", err)
log.Errorf("bot.hipchat.connect: Error=%s", err)
if err.Error() == "could not authenticate" {
return nil
}
@ -109,9 +108,10 @@ func connect(username, password string) *hipchat.Client {
return nil
}
if client != nil && err == nil {
log.Info("Successfully connected to hipchat server")
return client
}
log.Debugf("wait fo 30 seconds")
log.Debugln("Can not connect to hipcaht now, wait fo 30 seconds")
time.Sleep(30 * time.Second)
attempts--
}
@ -138,7 +138,6 @@ func new(name, username, pass, approvalsChannel string, k8sImplementer kubernete
// Start the bot
func (b *Bot) Start(ctx context.Context) error {
log.Debugln(">>> bot.hipchat.Start()")
if b.hipchatClient == nil {
return errors.New("could not conect to hipchat server")
@ -161,9 +160,9 @@ func (b *Bot) Start(ctx context.Context) error {
func (b *Bot) startInternal() error {
client := b.hipchatClient
log.Debugf("startInternal(): channel=%s, userName=%s\n", b.approvalsChannel, b.name)
client.Status("chat") // chat, away or idle
client.Status("chat")
client.Join(b.approvalsChannel, b.name)
b.postMessage("Keel bot started ...")
go client.KeepAlive()
go func() {
for {
@ -177,25 +176,14 @@ func (b *Bot) startInternal() error {
return nil
}
// // A Message represents a message received from HipChat.
// type Message struct {
// From string
// To string
// Body string
// MentionName string
// }
// Body:"@IgorKomlew release notification from keel"
// hipchat.handleMessage(): &hipchat.Message{From:"701032_keel-bot@conf.hipchat.com", To:"701032_4966430@chat.hipchat.com/bot", Body:"release notification from keel", MentionName:""}
func (b *Bot) handleMessage(message *hipchat.Message) {
msg := b.trimXMPPMessage(message)
log.Debugf("hipchat.handleMessage(): %#v // %#v\n", message, msg)
if msg.From == "" || msg.To == "" {
log.Debugf("hipchat.handleMessage(): ignore")
log.Debugln("hipchat.handleMessage(): fields 'From:' or 'To:' are empty, ignore")
return
}
if !b.isBotMessage(msg) {
log.Debugf("hipchat.handleMessage(): is not a bot message")
return
}
@ -207,8 +195,7 @@ func (b *Bot) handleMessage(message *hipchat.Message) {
if responseLines, ok := bot.BotEventTextToResponse[msg.Body]; ok {
response := strings.Join(responseLines, "\n")
fmt.Println(">>> " + response)
b.respond(response)
b.respond(formatAsSnippet(response))
return
}
@ -225,16 +212,37 @@ func (b *Bot) handleMessage(message *hipchat.Message) {
}).Debug("handleMessage: bot couldn't recognise command")
}
func (b *Bot) respond(response string) {
b.hipchatClient.Say(b.approvalsChannel, b.name, response)
func (b *Bot) handleCommand(message *hipchat.Message) {
eventText := message.Body
switch eventText {
case "get deployments":
log.Info("getting deployments")
response := bot.DeploymentsResponse(bot.Filter{}, b.k8sImplementer)
b.respond(formatAsSnippet(response))
return
case "get approvals":
log.Info("getting approvals")
response := bot.ApprovalsResponse(b.approvalsManager)
b.respond(formatAsSnippet(response))
return
}
// handle dynamic commands
if strings.HasPrefix(eventText, bot.RemoveApprovalPrefix) {
id := strings.TrimSpace(strings.TrimPrefix(eventText, bot.RemoveApprovalPrefix))
snippet := bot.RemoveApprovalHandler(id, b.approvalsManager)
b.respond(formatAsSnippet(snippet))
return
}
log.Info("hipchat.handleCommand(): command not found")
}
func (b *Bot) handleCommand(message *hipchat.Message) {
fmt.Printf("bot.hipchat.handleCommand() %v\n", message)
func formatAsSnippet(msg string) string {
return "/code " + msg
}
func (b *Bot) isCommand(message *hipchat.Message) bool {
fmt.Printf("bot.hipchat.isCommand=%s\n", message.Body)
if bot.StaticBotCommands[message.Body] {
return true
@ -261,12 +269,12 @@ func (b *Bot) trimXMPPMessage(message *hipchat.Message) *hipchat.Message {
func trimMentionName(message string) string {
re := regexp.MustCompile(`^(@\w+)`)
match := re.FindStringSubmatch(message)
match := re.FindStringSubmatch(strings.TrimSpace(message))
if match == nil {
return ""
}
if len(match) != 0 {
return match[1]
return strings.TrimSpace(match[1])
}
return ""
}
@ -284,14 +292,15 @@ func (b *Bot) trimUser(user string) string {
}
func (b *Bot) postMessage(msg string) error {
log.Debugf(">>> bot.hipchat.postMessage: %s\n", msg)
b.hipchatClient.Say(b.approvalsChannel, b.name, msg)
// b.respond(msg)
return nil
}
func (b *Bot) respond(response string) {
b.hipchatClient.Say(b.approvalsChannel, b.name, response)
}
func (b *Bot) trimBot(msg string) string {
// msg = strings.Replace(msg, strings.ToLower(b.msgPrefix), "", 1)
msg = strings.TrimPrefix(msg, b.mentionName)
msg = strings.Trim(msg, "\n")
msg = strings.TrimSpace(msg)

29
bot/hipchat/templates.go Normal file
View File

@ -0,0 +1,29 @@
package hipchat
var ApprovalRequiredTempl = `Approval required!
%s
To vote for change type '%s approve %s'
To reject it: '%s reject %s'
Votes: %d/%d
Delta: %s
Identifier: %s
Provider: %s`
var VoteReceivedTempl = `Vote received
Waiting for remaining votes!
Votes: %d/%d
Delta: %s
Identifier: %s`
var ChangeRejectedTempl = `Change rejected
Change was rejected.
Status: %s
Votes: %d/%d
Delta: %s
Identifier: %s`
var UpdateApprovedTempl = `Update approved!
All approvals received, thanks for voting!
Votes: %d/%d
Delta: %s
Identifier: %s`

View File

@ -1,12 +1,10 @@
package slack
import (
"bytes"
"fmt"
"strings"
"github.com/keel-hq/keel/bot"
"github.com/keel-hq/keel/bot/formatter"
"github.com/keel-hq/keel/types"
"github.com/nlopes/slack"
@ -252,36 +250,3 @@ func (b *Bot) replyToApproval(approval *types.Approval) error {
}
return nil
}
func (b *Bot) approvalsResponse() string {
approvals, err := b.approvalsManager.List()
if err != nil {
return fmt.Sprintf("got error while fetching approvals: %s", err)
}
if len(approvals) == 0 {
return fmt.Sprintf("there are currently no request waiting to be approved.")
}
buf := &bytes.Buffer{}
approvalCtx := formatter.Context{
Output: buf,
Format: formatter.NewApprovalsFormat(formatter.TableFormatKey, false),
}
err = formatter.ApprovalWrite(approvalCtx, approvals)
if err != nil {
return fmt.Sprintf("got error while formatting approvals: %s", err)
}
return buf.String()
}
func (b *Bot) removeApprovalHandler(identifier string) string {
err := b.approvalsManager.Delete(identifier)
if err != nil {
return fmt.Sprintf("failed to remove '%s' approval: %s.", identifier, err)
}
return fmt.Sprintf("approval '%s' removed.", identifier)
}

View File

@ -312,18 +312,20 @@ func (b *Bot) handleCommand(event *slack.MessageEvent, eventText string) {
switch eventText {
case "get deployments":
log.Info("getting deployments")
response := b.deploymentsResponse(Filter{})
response := bot.DeploymentsResponse(bot.Filter{}, b.k8sImplementer)
b.respond(event, formatAsSnippet(response))
return
case "get approvals":
response := b.approvalsResponse()
response := bot.ApprovalsResponse(b.approvalsManager)
b.respond(event, formatAsSnippet(response))
return
}
// handle dynamic commands
if strings.HasPrefix(eventText, bot.RemoveApprovalPrefix) {
b.respond(event, formatAsSnippet(b.removeApprovalHandler(strings.TrimSpace(strings.TrimPrefix(eventText, bot.RemoveApprovalPrefix)))))
id := strings.TrimSpace(strings.TrimPrefix(eventText, bot.RemoveApprovalPrefix))
snippet := bot.RemoveApprovalHandler(id, b.approvalsManager)
b.respond(event, formatAsSnippet(snippet))
return
}