Skip to content

Commit

Permalink
Issue #37: Added multi-cluster support
Browse files Browse the repository at this point in the history
Modified implementations and updated botkube help. Removed CheckChannel value from config and helm charts.
  • Loading branch information
mugdha-adhav committed Feb 25, 2019
1 parent 8e1a433 commit e521932
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 74 deletions.
2 changes: 0 additions & 2 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -125,5 +125,3 @@ settings:
clustername: not-configured
# Set false to disable kubectl commands execution
allowkubectl: false
# Set true only respond to channel in config
checkchannel: false
2 changes: 1 addition & 1 deletion design/multi-cluster.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ To get the list of all clusters configured in the slack, you can run the followi

##### Workflow

![alt text](https://raw.githubusercontent.com/mugdha-adhav/botkube/infracloudio/add-cluster-name/design/workflow.png)
![Multi_Cluster_Design](https://raw.githubusercontent.com/infracloudio/botkube/infracloudio/add-cluster-name/design/workflow.png)


### Drawbacks
Expand Down
2 changes: 0 additions & 2 deletions helm/botkube/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,6 @@ config:
clustername: not-configured
# Set false to disable kubectl commands execution
allowkubectl: false
# Set true only respond to channel in config
checkchannel: false

resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
Expand Down
1 change: 0 additions & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ type Slack struct {
type Settings struct {
ClusterName string
AllowKubectl bool
CheckChannel bool
}

// New returns new Config
Expand Down
201 changes: 139 additions & 62 deletions pkg/execute/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,14 @@ var validKubectlCommands = map[string]bool{
"auth": true,
}

var validNotifierCommands = map[string]bool{
var validNotifierCommand = map[string]bool{
"notifier": true,
"help": true,
"ping": true,
}
var validPingCommand = map[string]bool{
"ping": true,
}
var validHelpCommand = map[string]bool{
"help": true,
}

var kubectlBinary = "/usr/local/bin/kubectl"
Expand All @@ -48,17 +52,52 @@ type Executor interface {

// DefaultExecutor is a default implementations of Executor
type DefaultExecutor struct {
Message string
AllowKubectl bool
ClusterName string
Message string
AllowKubectl bool
ClusterName string
ChannelName string
IsAuthChannel bool
}

// NotifierAction creates custom type for notifier actions
type NotifierAction string

// Defines constants for notifier actions
const (
Start NotifierAction = "start"
Stop NotifierAction = "stop"
Status NotifierAction = "status"
ShowConfig NotifierAction = "showconfig"
)

func (action NotifierAction) String() string {
return string(action)
}

// CommandFlags creates custom type for flags in botkube
type CommandFlags string

// Defines botkube flags
const (
ClusterFlag CommandFlags = "--cluster-name"
FollowFlag CommandFlags = "--follow"
AbbrFollowFlag CommandFlags = "-f"
WatchFlag CommandFlags = "--watch"
AbbrWatchFlag CommandFlags = "-w"
)

func (flag CommandFlags) String() string {
return string(flag)
}

// NewDefaultExecutor returns new Executor object
func NewDefaultExecutor(msg string, allowkubectl bool, clustername string) Executor {
func NewDefaultExecutor(msg string, allowkubectl bool, clusterName, channelName string, isAuthChannel bool) Executor {
return &DefaultExecutor{
Message: msg,
AllowKubectl: allowkubectl,
ClusterName: clustername,
Message: msg,
AllowKubectl: allowkubectl,
ClusterName: clusterName,
ChannelName: channelName,
IsAuthChannel: isAuthChannel,
}
}

Expand All @@ -69,35 +108,44 @@ func (e *DefaultExecutor) Execute() string {
if !e.AllowKubectl {
return fmt.Sprintf(kubectlDisabledMsg, e.ClusterName)
}
return fmt.Sprintf("Cluster: %s\n%s", e.ClusterName, runKubectlCommand(args))
return runKubectlCommand(args, e.ClusterName, e.IsAuthChannel)
}
if validNotifierCommands[args[0]] {
return runNotifierCommand(args, e.ClusterName)
if validNotifierCommand[args[0]] {
return runNotifierCommand(args, e.ClusterName, e.IsAuthChannel)
}
if validPingCommand[args[0]] {
return runPingCommand(args, e.ClusterName)
}
if validHelpCommand[args[0]] {
return printHelp(e.ChannelName)
}
return unsupportedCmdMsg
}

func printHelp() string {
func printHelp(channelName string) string {
allowedKubectl := ""
for k := range validKubectlCommands {
allowedKubectl = allowedKubectl + k + ", "
}
helpMsg := "BotKube executes kubectl commands on k8s cluster and returns output.\n" +
"Usages:\n" +
" @BotKube <kubectl command without `kubectl` prefix>\n" +
" @BotKube <kubectl command without `kubectl` prefix> [" + ClusterFlag.String() + " <cluster_name>]\n" +
"e.g:\n" +
" @BotKube get pods\n" +
" @BotKube logs podname -n namespace\n" +
" @BotKube get deployment " + ClusterFlag.String() + " cluster_name\n" +
"Allowed kubectl commands:\n" +
" " + allowedKubectl + "\n\n" +
"Commands to manage notifier:\n" +
"notifier stop Stop sending k8s event notifications to Slack (started by default)\n" +
"notifier start Start sending k8s event notifications to Slack\n" +
"notifier status Show running status of event notifier\n" +
"notifier showconfig Show BotKube configuration for event notifier\n\n" +
"Commands to manage notifier (Runs only on configured channel " + channelName + "):\n" +
"@BotKube notifier stop Stop sending k8s event notifications to Slack (started by default)\n" +
"@BotKube notifier start Start sending k8s event notifications to Slack\n" +
"@BotKube notifier status Show running status of event notifier\n" +
"@BotKube notifier showconfig Show BotKube configuration for event notifier\n\n" +
"Other Commands:\n" +
"help Show help\n" +
"ping Check connection health\n"
"@BotKube help Show help\n" +
"@BotKube ping Check connection health\n\n" +
"Options:\n" +
"" + ClusterFlag.String() + " Get cluster specific response\n"
return helpMsg

}
Expand All @@ -106,71 +154,100 @@ func printDefaultMsg() string {
return unsupportedCmdMsg
}

func runKubectlCommand(args []string) string {
func runKubectlCommand(args []string, clusterName string, isAuthChannel bool) string {
// Use 'default' as a default namespace
args = append([]string{"-n", "default"}, args...)

// Remove unnecessary flags
finalArgs := []string{}
for _, a := range args {
if a == "-f" || strings.HasPrefix(a, "--follow") {
checkFlag := false
for _, arg := range args {
if checkFlag {
if arg != clusterName {
return ""
}
checkFlag = false
continue
}
if a == "-w" || strings.HasPrefix(a, "--watch") {
if arg == AbbrFollowFlag.String() || strings.HasPrefix(arg, FollowFlag.String()) {
continue
}
finalArgs = append(finalArgs, a)
if arg == AbbrWatchFlag.String() || strings.HasPrefix(arg, WatchFlag.String()) {
continue
}
if strings.HasPrefix(arg, ClusterFlag.String()) {
if arg == ClusterFlag.String() {
checkFlag = true
} else if strings.SplitAfterN(arg, ClusterFlag.String()+"=", 2)[1] != clusterName {
return ""
}
isAuthChannel = true
continue
}
finalArgs = append(finalArgs, arg)
}
if isAuthChannel == false {
return ""
}

cmd := exec.Command(kubectlBinary, finalArgs...)
out, err := cmd.CombinedOutput()
if err != nil {
log.Logger.Error("Error in executing kubectl command: ", err)
return string(out) + err.Error()
return fmt.Sprintf("Cluster: %s\n%s", clusterName, string(out)+err.Error())
}
return string(out)
return fmt.Sprintf("Cluster: %s\n%s", clusterName, string(out))
}

// TODO: Have a seperate cli which runs bot commands
func runNotifierCommand(args []string, clustername string) string {
switch len(args) {
case 1:
if strings.ToLower(args[0]) == "help" {
return printHelp()
}
if strings.ToLower(args[0]) == "ping" {
return fmt.Sprintf("pong from cluster '%s'", clustername)
}
case 2:
if args[0] != "notifier" {
return printDefaultMsg()
}
if args[1] == "start" {
config.Notify = true
log.Logger.Info("Notifier enabled")
return fmt.Sprintf(notifierStartMsg, clustername)
func runNotifierCommand(args []string, clusterName string, isAuthChannel bool) string {
if isAuthChannel == false {
return ""
}
switch args[1] {
case Start.String():
config.Notify = true
log.Logger.Info("Notifier enabled")
return fmt.Sprintf(notifierStartMsg, clusterName)
case Stop.String():
config.Notify = false
log.Logger.Info("Notifier disabled")
return fmt.Sprintf(notifierStopMsg, clusterName)
case Status.String():
if config.Notify == false {
return fmt.Sprintf("Notifications are off for cluster '%s'", clusterName)
}
if args[1] == "stop" {
config.Notify = false
log.Logger.Info("Notifier disabled")
return fmt.Sprintf(notifierStopMsg, clustername)
return fmt.Sprintf("Notifications are on for cluster '%s'", clusterName)
case ShowConfig.String():
out, err := showControllerConfig()
if err != nil {
log.Logger.Error("Error in executing showconfig command: ", err)
return "Error in getting configuration!"
}
if args[1] == "status" {
if config.Notify == false {
return fmt.Sprintf("Notifications are off for cluster '%s'", clustername)
return fmt.Sprintf("Showing config for cluster '%s'\n\n%s", clusterName, out)
}
return printDefaultMsg()
}

func runPingCommand(args []string, clusterName string) string {
checkFlag := false
for _, arg := range args {
if checkFlag {
if arg != clusterName {
return ""
}
return fmt.Sprintf("Notifications are on for cluster '%s'", clustername)
checkFlag = false
continue
}
if args[1] == "showconfig" {
out, err := showControllerConfig()
if err != nil {
log.Logger.Error("Error in executing showconfig command: ", err)
return "Error in getting configuration!"
if strings.HasPrefix(arg, ClusterFlag.String()) {
if arg == ClusterFlag.String() {
checkFlag = true
} else if strings.SplitAfterN(arg, ClusterFlag.String()+"=", 2)[1] != clusterName {
return ""
}
return fmt.Sprintf("Showing config for cluster '%s'\n\n%s", clustername, out)
continue
}
}
return printDefaultMsg()
return fmt.Sprintf("pong from cluster '%s'", clusterName)
}

func showControllerConfig() (string, error) {
Expand Down
17 changes: 11 additions & 6 deletions pkg/slack/slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type Bot struct {
type slackMessage struct {
ChannelID string
BotID string
MessageType string
InMessage string
OutMessage string
OutMsgLength int
Expand Down Expand Up @@ -55,6 +56,7 @@ func (b *Bot) Start() {
go rtm.ManageConnection()

for msg := range rtm.IncomingEvents {
isAuthChannel := false
switch ev := msg.Data.(type) {
case *slack.ConnectedEvent:
logging.Logger.Debug("Connection Info: ", ev.Info)
Expand All @@ -73,10 +75,9 @@ func (b *Bot) Start() {
if !strings.HasPrefix(ev.Text, "<@"+botID+"> ") {
continue
}
// if config.settings.checkChannel is true
// Serve only if current channel is in config
if b.CheckChannel && (b.ChannelName != info.Name) {
continue
if b.ChannelName == info.Name {
isAuthChannel = true
}
}
}
Expand All @@ -95,7 +96,7 @@ func (b *Bot) Start() {
InMessage: inMessage,
RTM: rtm,
}
sm.HandleMessage(b.AllowKubectl, b.ClusterName)
sm.HandleMessage(b.AllowKubectl, b.ClusterName, b.ChannelName, isAuthChannel)

case *slack.RTMError:
logging.Logger.Errorf("Slack RMT error: %+v", ev.Error())
Expand All @@ -108,8 +109,8 @@ func (b *Bot) Start() {
}
}

func (sm *slackMessage) HandleMessage(allowkubectl bool, clustername string) {
e := execute.NewDefaultExecutor(sm.InMessage, allowkubectl, clustername)
func (sm *slackMessage) HandleMessage(allowkubectl bool, clusterName, channelName string, isAuthChannel bool) {
e := execute.NewDefaultExecutor(sm.InMessage, allowkubectl, clusterName, channelName, isAuthChannel)
sm.OutMessage = e.Execute()
sm.OutMsgLength = len(sm.OutMessage)
sm.Send()
Expand Down Expand Up @@ -141,7 +142,11 @@ func (sm slackMessage) Send() {
logging.Logger.Error("Error in uploading file:", err)
}
return
} else if sm.OutMsgLength == 0 {
logging.Logger.Info("Invalid request. Dumping the response")
return
}

params := slack.PostMessageParameters{
AsUser: true,
}
Expand Down

0 comments on commit e521932

Please sign in to comment.