Skip to content
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

Send the cluster name for all interactive commands #1091

Merged
merged 1 commit into from
Jun 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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