Skip to content

Commit

Permalink
Merge pull request #29 from rusenask/feature/slack_integration
Browse files Browse the repository at this point in the history
Feature/slack integration
  • Loading branch information
rusenask authored Jul 8, 2017
2 parents 1267be2 + dbaf754 commit 9feb21b
Show file tree
Hide file tree
Showing 793 changed files with 288,078 additions and 17 deletions.
237 changes: 237 additions & 0 deletions bot/bot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
package bot

import (
"context"
"errors"
"fmt"
"strings"

"github.com/nlopes/slack"

"github.com/rusenask/keel/provider/kubernetes"

log "github.com/Sirupsen/logrus"
)

var (
botEventTextToResponse = map[string][]string{
"help": {
`Here's a list of supported commands`,
`- "get deployments" -> get a list of all deployments`,
// `- "get deployments all" -> get a list of all deployments`,
// `- "describe deployment <deployment>" -> get details for specified deployment`,
},
}

// static bot commands can be used straight away
staticBotCommands = map[string]bool{
"get deployments": true,
"get deployments all": true,
}

// dynamic bot command prefixes have to be matched
dynamicBotCommandPrefixes = []string{"describe deployment"}
)

type Bot struct {
id string // bot id
name string // bot name

users map[string]string

msgPrefix string

slackClient *slack.Client
slackRTM *slack.RTM

k8sImplementer kubernetes.Implementer

ctx context.Context
}

func New(name, token string, k8sImplementer kubernetes.Implementer) *Bot {
client := slack.New(token)

return &Bot{
slackClient: client,
k8sImplementer: k8sImplementer,
name: name,
}
}

// Start - start bot
func (b *Bot) Start(ctx context.Context) error {

// setting root context
b.ctx = ctx

users, err := b.slackClient.GetUsers()
if err != nil {
return err
}

b.users = map[string]string{}

for _, user := range users {
switch user.Name {
case b.name:
if user.IsBot {
b.id = user.ID
}
default:
continue
}
}
if b.id == "" {
return errors.New("could not find bot in the list of names, check if the bot is called \"" + b.name + "\" ")
}

b.msgPrefix = strings.ToLower("<@" + b.id + ">")

go b.startInternal()

return nil
}

func (b *Bot) startInternal() error {
b.slackRTM = b.slackClient.NewRTM()

go b.slackRTM.ManageConnection()

for {
select {
case <-b.ctx.Done():
return nil

case msg := <-b.slackRTM.IncomingEvents:
switch ev := msg.Data.(type) {
case *slack.HelloEvent:
// Ignore hello

case *slack.ConnectedEvent:
// fmt.Println("Infos:", ev.Info)
// fmt.Println("Connection counter:", ev.ConnectionCount)
// Replace #general with your Channel ID
// b.slackRTM.SendMessage(b.slackRTM.NewOutgoingMessage("Hello world", "#general"))

case *slack.MessageEvent:
b.handleMessage(ev)
case *slack.PresenceChangeEvent:
// fmt.Printf("Presence Change: %v\n", ev)

// case *slack.LatencyReport:
// fmt.Printf("Current latency: %v\n", ev.Value)

case *slack.RTMError:
fmt.Printf("Error: %s\n", ev.Error())

case *slack.InvalidAuthEvent:
fmt.Printf("Invalid credentials")
return fmt.Errorf("invalid credentials")

default:

// Ignore other events..
// fmt.Printf("Unexpected: %v\n", msg.Data)
}
}
}
}

func (b *Bot) handleMessage(event *slack.MessageEvent) {
if event.BotID != "" || event.User == "" || event.SubType == "bot_message" {
log.WithFields(log.Fields{
"event_bot_ID": event.BotID,
"event_user": event.User,
"event_subtype": event.SubType,
}).Info("handleMessage: ignoring message")
return
}

eventText := strings.Trim(strings.ToLower(event.Text), " \n\r")

// All messages past this point are directed to @gopher itself
if !b.isBotMessage(event, eventText) {
log.Info("not a bot message")
return
}

eventText = b.trimBot(eventText)

// Responses that are just a canned string response
if responseLines, ok := botEventTextToResponse[eventText]; ok {
response := strings.Join(responseLines, "\n")
b.respond(event, formatAsSnippet(response))
return
}

if b.isCommand(event, eventText) {
b.handleCommand(event, eventText)
return
}

log.WithFields(log.Fields{
"name": b.name,
"bot_id": b.id,
"command": eventText,
"untrimmed": strings.Trim(strings.ToLower(event.Text), " \n\r"),
}).Debug("handleMessage: bot couldn't recognise command")
}

func (b *Bot) isCommand(event *slack.MessageEvent, eventText string) bool {
if staticBotCommands[eventText] {
return true
}

for _, prefix := range dynamicBotCommandPrefixes {
if strings.HasPrefix(eventText, prefix) {
return true
}
}

return false
}

func (b *Bot) handleCommand(event *slack.MessageEvent, eventText string) {
switch eventText {
case "get deployments":
log.Info("getting deployments")
response := b.deploymentsResponse(Filter{})
b.respond(event, formatAsSnippet(response))
return
}

log.Info("command not found")
}

func (b *Bot) respond(event *slack.MessageEvent, response string) {
b.slackRTM.SendMessage(b.slackRTM.NewOutgoingMessage(response, event.Channel))
}

func (b *Bot) isBotMessage(event *slack.MessageEvent, eventText string) bool {
prefixes := []string{
b.msgPrefix,
"keel",
}

for _, p := range prefixes {
if strings.HasPrefix(eventText, p) {
return true
}
}

// Direct message channels always starts with 'D'
return strings.HasPrefix(event.Channel, "D")
}

func (b *Bot) trimBot(msg string) string {
msg = strings.Replace(msg, strings.ToLower(b.msgPrefix), "", 1)
msg = strings.TrimPrefix(msg, b.name)
msg = strings.Trim(msg, " :\n")

return msg
}

func formatAsSnippet(response string) string {
return "```" + response + "```"
}
94 changes: 94 additions & 0 deletions bot/deployments.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package bot

import (
"bytes"
"fmt"

"github.com/rusenask/keel/bot/formatter"

"k8s.io/client-go/pkg/apis/extensions/v1beta1"

log "github.com/Sirupsen/logrus"
)

type Filter struct {
Namespace string
All bool // keel or not
}

// deployments - gets all deployments
func (b *Bot) deployments() ([]v1beta1.Deployment, error) {
deploymentLists := []*v1beta1.DeploymentList{}

n, err := b.k8sImplementer.Namespaces()
if err != nil {
return nil, err
}

for _, n := range n.Items {
l, err := b.k8sImplementer.Deployments(n.GetName())
if err != nil {
log.WithFields(log.Fields{
"error": err,
"namespace": n.GetName(),
}).Error("provider.kubernetes: failed to list deployments")
continue
}
deploymentLists = append(deploymentLists, l)
}

impacted := []v1beta1.Deployment{}

for _, deploymentList := range deploymentLists {
for _, deployment := range deploymentList.Items {
impacted = append(impacted, deployment)
}
}

return impacted, nil
}

func (b *Bot) deploymentsResponse(filter Filter) string {
deps, err := b.deployments()
if err != nil {
return fmt.Sprintf("got error while fetching deployments: %s", err)
}
log.Debugf("%d deployments fetched, formatting", len(deps))
buf := &bytes.Buffer{}

DeploymentCtx := formatter.Context{
Output: buf,
Format: formatter.NewDeploymentsFormat(formatter.TableFormatKey, false),
}
err = formatter.DeploymentWrite(DeploymentCtx, convertToInternal(deps))

if err != nil {
return fmt.Sprintf(" got error while formatting deployments: %s", err)
}

return buf.String()
}

func convertToInternal(deployments []v1beta1.Deployment) []formatter.Deployment {
formatted := []formatter.Deployment{}
for _, d := range deployments {

formatted = append(formatted, formatter.Deployment{
Namespace: d.Namespace,
Name: d.Name,
Replicas: d.Status.Replicas,
AvailableReplicas: d.Status.AvailableReplicas,
Images: getImages(&d),
})
}
return formatted
}

func getImages(deployment *v1beta1.Deployment) []string {
var images []string
for _, c := range deployment.Spec.Template.Spec.Containers {
images = append(images, c.Image)
}

return images
}
51 changes: 51 additions & 0 deletions bot/formatter/custom.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package formatter

import (
"strings"
)

const (
imageHeader = "IMAGE"
createdSinceHeader = "CREATED"
createdAtHeader = "CREATED AT"
sizeHeader = "SIZE"
labelsHeader = "LABELS"
nameHeader = "NAME"
driverHeader = "DRIVER"
scopeHeader = "SCOPE"
)

type subContext interface {
FullHeader() string
AddHeader(header string)
}

// HeaderContext provides the subContext interface for managing headers
type HeaderContext struct {
header []string
}

// FullHeader returns the header as a string
func (c *HeaderContext) FullHeader() string {
if c.header == nil {
return ""
}
return strings.Join(c.header, "\t")
}

// AddHeader adds another column to the header
func (c *HeaderContext) AddHeader(header string) {
if c.header == nil {
c.header = []string{}
}
c.header = append(c.header, strings.ToUpper(header))
}

func stripNamePrefix(ss []string) []string {
sss := make([]string, len(ss))
for i, s := range ss {
sss[i] = s[1:]
}

return sss
}
Loading

0 comments on commit 9feb21b

Please sign in to comment.