Skip to content

Commit

Permalink
Merge branch 'main' into dependabot/go_modules/examples/src/golang.or…
Browse files Browse the repository at this point in the history
…g/x/image-0.18.0
  • Loading branch information
vaijab authored Jul 4, 2024
2 parents 75672a6 + 9b6b3ce commit f101c9b
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 43 deletions.
26 changes: 23 additions & 3 deletions pkg/bot/slack_cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
pb "github.com/kubeshop/botkube/pkg/api/cloudslack"
"github.com/kubeshop/botkube/pkg/bot/interactive"
"github.com/kubeshop/botkube/pkg/config"
conversationx "github.com/kubeshop/botkube/pkg/conversation"
"github.com/kubeshop/botkube/pkg/execute"
"github.com/kubeshop/botkube/pkg/execute/command"
"github.com/kubeshop/botkube/pkg/formatx"
Expand Down Expand Up @@ -87,7 +88,7 @@ func NewCloudSlack(log logrus.FieldLogger,
return nil, err
}

channels := slackChannelsConfigFrom(log, cfg.Channels)
channels := cloudSlackChannelsConfigFrom(log, cfg.Channels)
if err != nil {
return nil, fmt.Errorf("while producing channels configuration map by ID: %w", err)
}
Expand Down Expand Up @@ -573,15 +574,15 @@ func (b *CloudSlack) send(ctx context.Context, event slackMessage, resp interact
// TODO: Currently, we don't get the channel ID once we use modal. This needs to be investigated and fixed.
//
// we can open modal only if we have a TriggerID (it's available when user clicks a button)
//if resp.Type == api.PopupMessage && event.TriggerID != "" {
// if resp.Type == api.PopupMessage && event.TriggerID != "" {
// modalView := b.renderer.RenderModal(resp)
// modalView.PrivateMetadata = event.Channel
// _, err := b.client.OpenViewContext(ctx, event.TriggerID, modalView)
// if err != nil {
// return fmt.Errorf("while opening modal: %w", err)
// }
// return nil
//}
// }

options := []slack.MsgOption{
b.renderer.RenderInteractiveMessage(resp),
Expand Down Expand Up @@ -766,3 +767,22 @@ func (b *CloudSlack) GetStatus() health.PlatformStatus {
Reason: b.failureReason,
}
}

func cloudSlackChannelsConfigFrom(log logrus.FieldLogger, channelsCfg config.IdentifiableMap[config.CloudSlackChannel]) map[string]channelConfigByName {
channels := make(map[string]channelConfigByName)
for channAlias, channCfg := range channelsCfg {
normalizedChannelName, changed := conversationx.NormalizeChannelIdentifier(channCfg.Name)
if changed {
log.Warnf("Channel name %q has been normalized to %q", channCfg.Name, normalizedChannelName)
}
channCfg.Name = normalizedChannelName

channels[channCfg.Identifier()] = channelConfigByName{
ChannelBindingsByName: channCfg.ChannelBindingsByName,
alias: channAlias,
notify: !channCfg.Notification.Disabled,
}
}

return channels
}
42 changes: 34 additions & 8 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
koanfyaml "github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/providers/rawbytes"
"github.com/mitchellh/mapstructure"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
Expand Down Expand Up @@ -97,7 +98,7 @@ const (
// DiscordCommPlatformIntegration defines Discord integration.
DiscordCommPlatformIntegration CommPlatformIntegration = "discord"

//ElasticsearchCommPlatformIntegration defines Elasticsearch integration.
// ElasticsearchCommPlatformIntegration defines Elasticsearch integration.
ElasticsearchCommPlatformIntegration CommPlatformIntegration = "elasticsearch"

// WebhookCommPlatformIntegration defines an outgoing webhook integration.
Expand Down Expand Up @@ -195,6 +196,18 @@ type IncomingWebhook struct {
InClusterBaseURL string `yaml:"inClusterBaseURL"`
}

// CloudSlackChannel contains configuration bindings per channel.
type CloudSlackChannel struct {
ChannelBindingsByName `yaml:",inline" mapstructure:",squash"`

// ChannelID is the Slack ID of the channel.
// Currently, it is used for AI plugin as it has ability to fetch the Botkube Agent configuration.
// Later it can be used for deep linking to a given channel, see: https://api.slack.com/reference/deep-linking#app_channel
ChannelID string `yaml:"channelID"`
// Alias is an optional public alias for a private channel.
Alias *string `yaml:"alias,omitempty"`
}

// ChannelBindingsByName contains configuration bindings per channel.
type ChannelBindingsByName struct {
Name string `yaml:"name"`
Expand Down Expand Up @@ -498,12 +511,12 @@ type SocketSlack struct {

// CloudSlack configuration for multi-slack support
type CloudSlack struct {
Enabled bool `yaml:"enabled"`
Channels IdentifiableMap[ChannelBindingsByName] `yaml:"channels" validate:"required_if=Enabled true,dive,omitempty,min=1"`
Token string `yaml:"token"`
BotID string `yaml:"botID,omitempty"`
Server GRPCServer `yaml:"server"`
ExecutionEventStreamingDisabled bool `yaml:"executionEventStreamingDisabled"`
Enabled bool `yaml:"enabled"`
Channels IdentifiableMap[CloudSlackChannel] `yaml:"channels" validate:"required_if=Enabled true,dive,omitempty,min=1"`
Token string `yaml:"token"`
BotID string `yaml:"botID,omitempty"`
Server GRPCServer `yaml:"server"`
ExecutionEventStreamingDisabled bool `yaml:"executionEventStreamingDisabled"`
}

// GRPCServer config for gRPC server
Expand Down Expand Up @@ -715,7 +728,20 @@ func LoadWithDefaults(configs [][]byte) (*Config, LoadWithDefaultsDetails, error
}

var cfg Config
err = k.Unmarshal("", &cfg)
err = k.UnmarshalWithConf("", &cfg, koanf.UnmarshalConf{
DecoderConfig: &mapstructure.DecoderConfig{
Squash: true, // needed to properly unmarshal CloudSlack channel's ChannelBindingsByName

// also use defaults from koanf.UnmarshalWithConf
DecodeHook: mapstructure.ComposeDecodeHookFunc(
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToSliceHookFunc(","),
mapstructure.TextUnmarshallerHookFunc()),
Metadata: nil,
Result: &cfg,
WeaklyTypedInput: true,
},
})
if err != nil {
return nil, LoadWithDefaultsDetails{}, err
}
Expand Down
44 changes: 44 additions & 0 deletions pkg/config/redacted.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package config

import (
"fmt"
)

const redactedSecretStr = "*** REDACTED ***"

// HideSensitiveInfo removes sensitive information from the config.
func HideSensitiveInfo(in Config) Config {
out := in
// TODO: avoid printing sensitive data without need to resetting them manually (which is an error-prone approach)
for key, val := range out.Communications {
val.SocketSlack.AppToken = redactedSecretStr
val.SocketSlack.BotToken = redactedSecretStr
val.Elasticsearch.Password = redactedSecretStr
val.Discord.Token = redactedSecretStr
val.Mattermost.Token = redactedSecretStr
val.CloudSlack.Token = redactedSecretStr
// To keep the printed config readable, we don't print the certificate bytes.
val.CloudSlack.Server.TLS.CACertificate = nil
val.CloudTeams.Server.TLS.CACertificate = nil

// Replace private channel names with aliases
cloudSlackChannels := make(IdentifiableMap[CloudSlackChannel])
for _, channel := range val.CloudSlack.Channels {
if channel.Alias == nil {
cloudSlackChannels[channel.ChannelBindingsByName.Name] = channel
continue
}

outChannel := channel
outChannel.ChannelBindingsByName.Name = fmt.Sprintf("%s (public alias)", *channel.Alias)
outChannel.Alias = nil
cloudSlackChannels[*channel.Alias] = outChannel
}
val.CloudSlack.Channels = cloudSlackChannels

// maps are not addressable: https://stackoverflow.com/questions/42605337/cannot-assign-to-struct-field-in-a-map
out.Communications[key] = val
}

return out
}
34 changes: 4 additions & 30 deletions pkg/execute/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,40 +48,14 @@ func (e *ConfigExecutor) Commands() map[command.Verb]CommandFn {

// Show returns Config in yaml format
func (e *ConfigExecutor) Show(_ context.Context, cmdCtx CommandContext) (interactive.CoreMessage, error) {
cfg, err := e.renderBotkubeConfiguration()
redactedCfg := config.HideSensitiveInfo(e.cfg)
bytes, err := yaml.Marshal(redactedCfg)
if err != nil {
return interactive.CoreMessage{}, fmt.Errorf("while rendering Botkube configuration: %w", err)
}
return respond(cfg, cmdCtx), nil
}

const redactedSecretStr = "*** REDACTED ***"

func (e *ConfigExecutor) renderBotkubeConfiguration() (string, error) {
cfg := e.cfg

// hide sensitive info
// TODO: avoid printing sensitive data without need to resetting them manually (which is an error-prone approach)
for key, val := range cfg.Communications {
val.SocketSlack.AppToken = redactedSecretStr
val.SocketSlack.BotToken = redactedSecretStr
val.Elasticsearch.Password = redactedSecretStr
val.Discord.Token = redactedSecretStr
val.Mattermost.Token = redactedSecretStr
val.CloudSlack.Token = redactedSecretStr

// To keep the printed config readable, we don't print the certificate bytes.
val.CloudSlack.Server.TLS.CACertificate = nil
val.CloudTeams.Server.TLS.CACertificate = nil

// maps are not addressable: https://stackoverflow.com/questions/42605337/cannot-assign-to-struct-field-in-a-map
cfg.Communications[key] = val
}

b, err := yaml.Marshal(cfg)
if err != nil {
return "", err
return interactive.CoreMessage{}, fmt.Errorf("while rendering Botkube configuration: %w", err)
}

return string(b), nil
return respond(string(bytes), cmdCtx), nil
}
9 changes: 7 additions & 2 deletions test/cloud-slack-dev-e2e/botkube_page_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,20 +228,25 @@ func (p *BotkubeCloudPage) UpdateKubectlNamespace(t *testing.T) {
t.Log("Submitting changes")
p.page.MustWaitStable()
p.page.Screenshot("before-deploying-plugin-changes")
p.page.MustElementR("button", "/Deploy changes/i").MustClick()
p.page.MustElementR("button", "/Deploy changes/i").
MustWaitEnabled().
MustClick()
p.page.Screenshot("after-deploying-plugin-changes")
time.Sleep(3 * time.Second)
p.page.Screenshot("after-deploying-plugin-changes-later")
}

func (p *BotkubeCloudPage) VerifyUpdatedKubectlNamespace(t *testing.T) {
t.Log("Verifying that the 'namespace' value was updated and persisted properly")

time.Sleep(3 * time.Second)
p.openKubectlUpdateForm()
p.page.MustElementR("input#root_defaultNamespace", "kube-system")
}

func (p *BotkubeCloudPage) openKubectlUpdateForm() {
p.page.Screenshot("before-selecting-plugins-tab")
p.page.MustElementR(`div[role="tab"]`, "Plugins").MustFocus().MustClick().MustWaitStable()
p.page.MustElementR(`div[role="tab"]`, "Plugins").MustClick()

p.page.MustWaitStable()
p.page.Screenshot("after-selecting-plugins-tab")
Expand Down

0 comments on commit f101c9b

Please sign in to comment.