Skip to content

Commit

Permalink
Send the cluster name for all interactive commands
Browse files Browse the repository at this point in the history
  • Loading branch information
mszostok committed Jun 14, 2023
1 parent 428e624 commit 86075f9
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 19 deletions.
2 changes: 1 addition & 1 deletion cmd/botkube/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ func run(ctx context.Context) error {
}

if commGroupCfg.CloudSlack.Enabled {
sb, err := bot.NewCloudSlack(commGroupLogger.WithField(botLogFieldKey, "CloudSlack"), commGroupName, commGroupCfg.CloudSlack, executorFactory, reporter)
sb, err := bot.NewCloudSlack(commGroupLogger.WithField(botLogFieldKey, "CloudSlack"), commGroupName, commGroupCfg.CloudSlack, conf.Settings.ClusterName, executorFactory, reporter)
if err != nil {
return reportFatalError("while creating CloudSlack bot", err)
}
Expand Down
1 change: 1 addition & 0 deletions internal/executor/kubectl/builder/kubectl.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ func (e *Kubectl) Handle(ctx context.Context, cmd string, isInteractivitySupport
"resourceName": stateDetails.resourceName,
"resourceType": stateDetails.resourceType,
"verb": stateDetails.verb,
"cmd": cmd,
}).Debug("Extracted Slack state")

cmds := executorsRunner{
Expand Down
103 changes: 89 additions & 14 deletions pkg/api/message_bot_name.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package api

import (
"fmt"
"strings"
)

Expand All @@ -10,46 +11,67 @@ const (
maxReplaceNo = 100
)

// BotNameOption allows modifying ReplaceBotNamePlaceholder related options.
type BotNameOption func(opts *BotNameOptions)

// BotNameOptions holds options used in ReplaceBotNamePlaceholder func
type BotNameOptions struct {
ClusterName string
}

// BotNameWithClusterName sets the cluster name for places where MessageBotNamePlaceholder was also specified.
func BotNameWithClusterName(clusterName string) BotNameOption {
return func(opts *BotNameOptions) {
opts.ClusterName = clusterName
}
}

// ReplaceBotNamePlaceholder replaces bot name placeholder with a given name.
func (msg *Message) ReplaceBotNamePlaceholder(new string) {
func (msg *Message) ReplaceBotNamePlaceholder(new string, opts ...BotNameOption) {
var options BotNameOptions
for _, mutate := range opts {
mutate(&options)
}

for idx, item := range msg.Sections {
msg.Sections[idx].Buttons = ReplaceBotNameInButtons(item.Buttons, new)
msg.Sections[idx].PlaintextInputs = ReplaceBotNameInLabels(item.PlaintextInputs, new)
msg.Sections[idx].Selects = ReplaceBotNameInSelects(item.Selects, new)
msg.Sections[idx].MultiSelect = ReplaceBotNameInMultiSelect(item.MultiSelect, new)
msg.Sections[idx].Buttons = ReplaceBotNameInButtons(item.Buttons, new, options)
msg.Sections[idx].PlaintextInputs = ReplaceBotNameInLabels(item.PlaintextInputs, new, options)
msg.Sections[idx].Selects = ReplaceBotNameInSelects(item.Selects, new, options)
msg.Sections[idx].MultiSelect = ReplaceBotNameInMultiSelect(item.MultiSelect, new, options)

msg.Sections[idx].Base = ReplaceBotNameInBase(item.Base, new)
msg.Sections[idx].TextFields = ReplaceBotNameInTextFields(item.TextFields, new)
msg.Sections[idx].Context = ReplaceBotNameInContextItems(item.Context, new)
}
msg.PlaintextInputs = ReplaceBotNameInLabels(msg.PlaintextInputs, new)

msg.PlaintextInputs = ReplaceBotNameInLabels(msg.PlaintextInputs, new, options)
msg.BaseBody = ReplaceBotNameInBody(msg.BaseBody, new)
}

// ReplaceBotNameInButtons replaces bot name placeholder with a given name.
func ReplaceBotNameInButtons(btns Buttons, name string) Buttons {
func ReplaceBotNameInButtons(btns Buttons, name string, opts BotNameOptions) Buttons {
for i, item := range btns {
btns[i].Command = replace(item.Command, name)
btns[i].Command = commandReplaceWithAppend(item.Command, name, opts)
btns[i].Description = replace(btns[i].Description, name)
btns[i].Name = replace(btns[i].Name, name)
}
return btns
}

// ReplaceBotNameInLabels replaces bot name placeholder with a given name.
func ReplaceBotNameInLabels(labels LabelInputs, name string) LabelInputs {
func ReplaceBotNameInLabels(labels LabelInputs, name string, opts BotNameOptions) LabelInputs {
for i, item := range labels {
labels[i].Command = replace(item.Command, name)
labels[i].Command = commandReplacePrepend(item.Command, name, opts)
labels[i].Text = replace(item.Text, name)
labels[i].Placeholder = replace(item.Placeholder, name)
}
return labels
}

// ReplaceBotNameInSelects replaces bot name placeholder with a given name.
func ReplaceBotNameInSelects(selects Selects, name string) Selects {
func ReplaceBotNameInSelects(selects Selects, name string, opts BotNameOptions) Selects {
for i, item := range selects.Items {
selects.Items[i].Command = replace(item.Command, name)
selects.Items[i].Command = commandReplacePrepend(item.Command, name, opts)
selects.Items[i].Name = replace(item.Name, name)
selects.Items[i].OptionGroups = ReplaceBotNameInOptionGroups(item.OptionGroups, name)
selects.Items[i].InitialOption = ReplaceBotNameInOptionItem(item.InitialOption, name)
Expand All @@ -58,8 +80,8 @@ func ReplaceBotNameInSelects(selects Selects, name string) Selects {
}

// ReplaceBotNameInMultiSelect replaces bot name placeholder with a given name.
func ReplaceBotNameInMultiSelect(ms MultiSelect, name string) MultiSelect {
ms.Command = replace(ms.Command, name)
func ReplaceBotNameInMultiSelect(ms MultiSelect, name string, opts BotNameOptions) MultiSelect {
ms.Command = commandReplacePrepend(ms.Command, name, opts)
ms.Name = replace(ms.Name, name)
ms.Description = ReplaceBotNameInBody(ms.Description, name)
ms.InitialOptions = ReplaceBotNameInOptions(ms.InitialOptions, name)
Expand Down Expand Up @@ -130,3 +152,56 @@ func ReplaceBotNameInOptionGroups(groups []OptionGroup, name string) []OptionGro
func replace(text, new string) string {
return strings.Replace(text, MessageBotNamePlaceholder, new, maxReplaceNo)
}

func commandReplaceWithAppend(cmd, botName string, opts BotNameOptions) string {
if cmd == "" {
return cmd
}
if !strings.Contains(cmd, MessageBotNamePlaceholder) {
return cmd
}

cmd = replace(cmd, botName)
if opts.ClusterName == "" {
return cmd
}

return fmt.Sprintf("%s --cluster-name=%q ", cmd, opts.ClusterName)
}

func commandReplacePrepend(cmd, botName string, opts BotNameOptions) string {
if cmd == "" {
return cmd
}
cmd = replace(cmd, botName)

if !strings.Contains(cmd, MessageBotNamePlaceholder) {
return cmd
}
if opts.ClusterName == "" {
return cmd
}

parts := strings.SplitAfterN(cmd, botName, 2)
switch len(parts) {
case 0, 1: // if there is no bot name we don't need to add cluster name as this command won't be never executed against our instance
return cmd
default:
// we need to append the --cluster-name flag right after the `@Botkube {plugin_name}`
// As a result, we won't break the order of other flags.

tokenized := strings.Fields(parts[1])
if len(tokenized) < 2 {
return cmd
}

pluginName := tokenized[0]
// we cannot do `strings.Join` on tokenized slice, as we need to preserve all whitespaces that where declared by plugin
// e.g. `--filter=` is different from `--filter ` and we don't know which one was used, so we can break it if we won't preserve the space.
restMessage := strings.TrimPrefix(tokenized[0], parts[1])

cmd = fmt.Sprintf("%s %s --cluster-name=%q %s", botName, pluginName, opts.ClusterName, restMessage)
}

return cmd
}
6 changes: 4 additions & 2 deletions pkg/bot/cloudslack.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type CloudSlack struct {
renderer *SlackRenderer
channels map[string]channelConfigByName
notifyMutex sync.Mutex
clusterName string
}

// cloudSlackAnalyticsReporter defines a reporter that collects analytics data.
Expand All @@ -64,6 +65,7 @@ type cloudSlackAnalyticsReporter interface {
func NewCloudSlack(log logrus.FieldLogger,
commGroupName string,
cfg config.CloudSlack,
clusterName string,
executorFactory ExecutorFactory,
reporter cloudSlackAnalyticsReporter) (*CloudSlack, error) {
client := slack.New(cfg.Token)
Expand Down Expand Up @@ -94,6 +96,7 @@ func NewCloudSlack(log logrus.FieldLogger,
channels: channels,
client: client,
botID: cfg.BotID,
clusterName: clusterName,
realNamesForID: map[string]string{},
}, nil
}
Expand Down Expand Up @@ -330,7 +333,6 @@ func (b *CloudSlack) getRealNameWithFallbackToUserID(ctx context.Context, userID
func (b *CloudSlack) handleMessage(ctx context.Context, event socketSlackMessage) error {
// Handle message only if starts with mention
request, found := b.findAndTrimBotMention(event.Text)
// TODO: Add global bot id here
if !found {
b.log.Debugf("Ignoring message as it doesn't contain %q mention", b.botID)
return nil
Expand Down Expand Up @@ -384,7 +386,7 @@ func (b *CloudSlack) handleMessage(ctx context.Context, event socketSlackMessage
func (b *CloudSlack) send(ctx context.Context, event socketSlackMessage, resp interactive.CoreMessage) error {
b.log.Debugf("Sending message to channel %q: %+v", event.Channel, resp)

resp.ReplaceBotNamePlaceholder(b.BotName())
resp.ReplaceBotNamePlaceholder(b.BotName(), api.BotNameWithClusterName(b.clusterName))
markdown := b.renderer.MessageToMarkdown(resp)

if len(markdown) == 0 {
Expand Down
4 changes: 2 additions & 2 deletions pkg/execute/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,9 +287,9 @@ func appendByUserOnlyIfNeeded(cmd, user string, origin command.Origin) string {
return fmt.Sprintf("%s by %s", cmd, user)
}

func filterInput(id string) api.LabelInput {
func filterInput(cmd string) api.LabelInput {
return api.LabelInput{
Command: fmt.Sprintf("%s %s --filter=", api.MessageBotNamePlaceholder, id),
Command: fmt.Sprintf("%s %s --filter=", api.MessageBotNamePlaceholder, cmd),
DispatchedAction: api.DispatchInputActionOnEnter,
Placeholder: "String pattern to filter by",
Text: "Filter output",
Expand Down
33 changes: 33 additions & 0 deletions pkg/execute/plugin_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package execute
import (
"context"
"fmt"
"strings"

"github.com/sirupsen/logrus"
"github.com/slack-go/slack"
Expand Down Expand Up @@ -87,6 +88,10 @@ func (e *PluginExecutor) Execute(ctx context.Context, bindings []string, slackSt
return interactive.CoreMessage{}, fmt.Errorf("while getting concrete plugin client: %w", err)
}

if slackState != nil {
e.sanitizeSlackStateIDs(slackState)
}

resp, err := cli.Execute(ctx, executor.ExecuteInput{
Command: cmdCtx.CleanCmd,
Configs: configs,
Expand Down Expand Up @@ -126,6 +131,34 @@ func (e *PluginExecutor) Execute(ctx context.Context, bindings []string, slackSt
return out, nil
}

// sanitizeSlackStateIDs makes sure that the slack state doesn't contain the --cluster-name
// in the ids. Example state before sanitizing:
//
// Values: map[string]map[string]slack.BlockAction{
// "6f79d399-e607-4744-80e8-6eb8e5a7743e": map[string]slack.BlockAction{
// "kubectl @builder --resource-type --cluster-name=`"stage\"": slack.BlockAction{}
// }
// }
func (e *PluginExecutor) sanitizeSlackStateIDs(slackState *slack.BlockActionStates) {
flag := fmt.Sprintf("--cluster-name=%q ", e.cfg.Settings.ClusterName)

for id, blocks := range slackState.Values {
updated := make(map[string]slack.BlockAction, len(blocks))
for id, act := range blocks {
cleanID := strings.ReplaceAll(id, flag, "")
act.Value = strings.ReplaceAll(act.Value, flag, "")
act.SelectedOption.Value = strings.ReplaceAll(act.SelectedOption.Value, flag, "")
act.ActionID = strings.ReplaceAll(act.ActionID, flag, "")
act.BlockID = strings.ReplaceAll(act.BlockID, flag, "")
updated[cleanID] = act
}

delete(slackState.Values, id)
cleanID := strings.ReplaceAll(id, flag, "")
slackState.Values[cleanID] = updated
}
}

func (e *PluginExecutor) Help(ctx context.Context, bindings []string, cmdCtx CommandContext) (interactive.CoreMessage, error) {
e.log.WithFields(logrus.Fields{
"bindings": bindings,
Expand Down

0 comments on commit 86075f9

Please sign in to comment.