-
Notifications
You must be signed in to change notification settings - Fork 289
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
Webhook refactored after k8s plugin extraction #1008
Webhook refactored after k8s plugin extraction #1008
Conversation
pkg/notifier/notifier.go
Outdated
// SendMessageToAll is used for notifying about Botkube start/stop listening, possible Botkube upgrades and other events. | ||
// Some integrations may decide to ignore such messages and have SendMessage method no-op. | ||
// TODO: Consider option per channel to turn on/off "announcements" (Botkube start/stop/upgrade, notify/config change). | ||
SendMessageToAll(context.Context, interactive.CoreMessage) error | ||
|
||
// SendMessage sends a generic message for a given source bindings. | ||
SendMessage(context.Context, interactive.CoreMessage, []string) error | ||
SendMessage(context.Context, interactive.CoreMessage, []string, any) error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bots don't consume raw event, so, so can we specify a different interface for Sinks and Bots if they are different?
So for sinks we can still require such method:
type Sink interface {
SendEvent(ctx context.Context, event any, eventSources []string) error
// (...)
}
and for Bots:
type Bot interface {
SendMessage(context.Context, interactive.CoreMessage, []string) error
// (...)
}
And then in dispatcher, instead of iterating over notifiers, we can iterate over both bots and sinks and send the data properly (message + raw data).
Could you please update also Elasticsearch integration? It can be one PR if it will be easier for you 👍 Thank you!
BTW We can remove SendEvent
methods from bots as they are not used and won't be in such setup 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need @mszostok 's input here, I don't want to break something :) After that I will introduce 2 interfaces.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, I agree with Paweł's comment 👍 For now, Bots don't need to have the raw event and I don't see any use case for that at least for the current features that we plan to have. Having that separated will be better for our code-base 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pkg/sink/webhook.go
Outdated
Source: sources[0], | ||
Data: rawData, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 That changes the contract which were before. Can we try to keep the same shape of data? so basically we can inline data.
And why do we send just a first source binding which instead of all of them?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually we cannot keep the previous format since, that format was all about kubernetes events. I applied same strategy we did for telemetry. So, those information is called RawData and we send it to webhook and es. Notice that, those data is coming from plugins which can be anything
We haven't changed the signature for sources yet, it is still accepting list of sources, and that is what I get only first since there is only one source inside it. this will be removed soon, and we will have only single source.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, fine with me - but please make sure we mention this change in the breaking changes 🙂
I understand that there is one item, but let's do this in the meantime it is not yet refactored to a single source:
Source: sources[0], | |
Data: rawData, | |
Source: strings.Join(sources, ","), | |
Data: rawData, |
Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is good suggestion :)
e099ae7
to
05be36f
Compare
bb0dfbb
to
8e1584c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Works well! Just minor code-related suggestions.
pkg/notifier/sink.go
Outdated
// SendMessage sends a generic message for a given source bindings. | ||
SendMessage(context.Context, any, []string) error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it could stay as SendEvent
, as we don't send message, but raw event data? What do you think?
bots = map[string]bot.Bot{} | ||
botNotifiers []notifier.Bot | ||
sinkNotifiers []notifier.Sink | ||
bots = map[string]bot.Bot{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
botNotifiers []notifier.Bot
is redundant aa we have this bot map. This map type can be changed to
bots = map[string]notifier.Bot{}
and essentially pass it everywhere instead of the slice. But we can do it later - can you change the type and add this TODO at least? Thanks!
bots = map[string]bot.Bot{} | |
// TODO: Use bots everywhere instead of `botNotifiers` slice | |
bots = map[string]notifier.Bot{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bot.Bot
refers to bot implementation while bot.Notifier
is for notification. When I compare with main, we have bots
and notifiers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Already discussed f2f 🙂 tl;dr for others: we use bots map for sendHelp
where we need only the SendMessageToAll
internal/source/dispatcher.go
Outdated
for _, n := range d.sinkNotifiers { | ||
go func(n notifier.Sink) { | ||
defer analytics.ReportPanicIfOccurs(d.log, d.reporter) | ||
err := n.SendMessage(ctx, event.RawObject, sources) | ||
if err != nil { | ||
reportErr := d.reporter.ReportHandledEventError(analytics.ReportEvent{ | ||
IntegrationType: n.Type(), | ||
Platform: n.IntegrationName(), | ||
PluginName: pluginName, | ||
AnonymizedEventFields: event.AnalyticsLabels, | ||
}, err) | ||
if reportErr != nil { | ||
err = multierror.Append(err, fmt.Errorf("while reporting sink analytics: %w", reportErr)) | ||
} | ||
|
||
d.log.Errorf("while sending sink message: %s", err.Error()) | ||
} | ||
reportErr := d.reporter.ReportHandledEventSuccess(analytics.ReportEvent{ | ||
IntegrationType: n.Type(), | ||
Platform: n.IntegrationName(), | ||
PluginName: pluginName, | ||
AnonymizedEventFields: event.AnalyticsLabels, | ||
}) | ||
if reportErr != nil { | ||
d.log.Errorf("while reporting sink analytics: %w", err) | ||
} | ||
if err := d.reportAudit(ctx, pluginName, fmt.Sprintf("%v", event.RawObject), dispatch.sourceName); err != nil { | ||
d.log.Errorf("while reporting sink audit event: %s", err.Error()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just checked and this is duplicated code for bots. Can we unify it? At least the reporting part can be extracted to a separate method with all the error wrapping
For example something like this:
func (d *Dispatcher) dispatchMsg(ctx context.Context, event source.Event, dispatch PluginDispatch) {
var (
pluginName = dispatch.pluginName
sources = []string{dispatch.sourceName}
)
for _, n := range d.getBotNotifiers(dispatch) {
go func(n notifier.Bot) {
defer analytics.ReportPanicIfOccurs(d.log, d.reporter)
msg := interactive.CoreMessage{
Message: event.Message,
}
err := n.SendMessage(ctx, msg, sources)
if err != nil {
reportErr := d.reportError(err, n, pluginName, event)
if reportErr != nil {
err = multierror.Append(err, fmt.Errorf("while reporting error: %w", reportErr))
}
d.log.Errorf("while sending bot message: %s", err.Error())
// TODO(Huseyin): Shouldn't we have return here? Currently we don't, which is weird...
}
reportErr := d.reportSuccess(ctx, n, pluginName, event, dispatch.sourceName)
if reportErr != nil {
d.log.Error(err)
}
}(n)
}
for _, n := range d.sinkNotifiers {
go func(n notifier.Sink) {
defer analytics.ReportPanicIfOccurs(d.log, d.reporter)
err := n.SendMessage(ctx, event.RawObject, sources)
if err != nil {
reportErr := d.reportError(err, n, pluginName, event)
if reportErr != nil {
err = multierror.Append(err, fmt.Errorf("while reporting error: %w", reportErr))
}
d.log.Errorf("while sending sink message: %s", err.Error())
// TODO(Huseyin): Shouldn't we have return here? Currently we don't, which is weird...
}
reportErr := d.reportSuccess(ctx, n, pluginName, event, dispatch.sourceName)
if reportErr != nil {
d.log.Error(err)
}
}(n)
}
// execute actions
actions, err := d.actionProvider.RenderedActions(event.RawObject, sources)
if err != nil {
d.log.Errorf("while rendering automated actions: %s", err.Error())
return
}
for _, act := range actions {
d.log.Infof("Executing action %q (command: %q)...", act.DisplayName, act.Command)
genericMsg := d.actionProvider.ExecuteAction(ctx, act)
for _, n := range d.getBotNotifiers(dispatch) {
go func(n notifier.Bot) {
defer analytics.ReportPanicIfOccurs(d.log, d.reporter)
err := n.SendMessage(ctx, genericMsg, sources)
if err != nil {
d.log.Errorf("while sending action result message: %s", err.Error())
}
}(n)
}
}
}
type genericNotifier interface {
IntegrationName() config.CommPlatformIntegration
Type() config.IntegrationType
}
func (d *Dispatcher) reportSuccess(ctx context.Context, n genericNotifier, pluginName string, event source.Event, sourceName string) error {
errs := multierror.New()
reportErr := d.reporter.ReportHandledEventSuccess(analytics.ReportEvent{
IntegrationType: n.Type(),
Platform: n.IntegrationName(),
PluginName: pluginName,
AnonymizedEventFields: event.AnalyticsLabels,
})
if reportErr != nil {
errs = multierror.Append(errs, fmt.Errorf("while reporting %s analytics: %w", n.Type(), reportErr))
}
if err := d.reportAudit(ctx, pluginName, fmt.Sprintf("%v", event.RawObject), sourceName); err != nil {
errs = multierror.Append(errs, fmt.Errorf("while reporting %s audit event: %w", n.Type(), reportErr))
}
return errs.ErrorOrNil()
}
func (d *Dispatcher) reportError(err error, n genericNotifier, pluginName string, event source.Event) error {
reportErr := d.reporter.ReportHandledEventError(analytics.ReportEvent{
IntegrationType: n.Type(),
Platform: n.IntegrationName(),
PluginName: pluginName,
AnonymizedEventFields: event.AnalyticsLabels,
}, err)
if reportErr != nil {
return fmt.Errorf("while reporting %s analytics: %w", n.Type(), reportErr)
}
return nil
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update: after discussion let's go with this snippet:
func (d *Dispatcher) dispatchMsg(ctx context.Context, event source.Event, dispatch PluginDispatch) {
var (
pluginName = dispatch.pluginName
sources = []string{dispatch.sourceName}
)
for _, n := range d.getBotNotifiers(dispatch) {
go func(n notifier.Bot) {
defer analytics.ReportPanicIfOccurs(d.log, d.reporter)
msg := interactive.CoreMessage{
Message: event.Message,
}
err := n.SendMessage(ctx, msg, sources)
if err != nil {
reportErr := d.reportError(ctx, err, n, pluginName, event, dispatch.sourceName
if reportErr != nil {
err = multierror.Append(err, fmt.Errorf("while reporting error: %w", reportErr))
}
d.log.Errorf("while sending bot message: %s", err.Error())
return
}
reportErr := d.reportSuccess(ctx, n, pluginName, event, dispatch.sourceName)
if reportErr != nil {
d.log.Error(err)
}
}(n)
}
for _, n := range d.sinkNotifiers {
go func(n notifier.Sink) {
defer analytics.ReportPanicIfOccurs(d.log, d.reporter)
err := n.SendMessage(ctx, event.RawObject, sources)
if err != nil {
reportErr := d.reportError(ctx, err, n, pluginName, event, dispatch.sourceName)
if reportErr != nil {
err = multierror.Append(err, fmt.Errorf("while reporting error: %w", reportErr))
}
d.log.Errorf("while sending sink message: %s", err.Error())
return
}
reportErr := d.reportSuccess(ctx, n, pluginName, event, dispatch.sourceName)
if reportErr != nil {
d.log.Error(err)
}
}(n)
}
// execute actions
actions, err := d.actionProvider.RenderedActions(event.RawObject, sources)
if err != nil {
d.log.Errorf("while rendering automated actions: %s", err.Error())
return
}
for _, act := range actions {
d.log.Infof("Executing action %q (command: %q)...", act.DisplayName, act.Command)
genericMsg := d.actionProvider.ExecuteAction(ctx, act)
for _, n := range d.getBotNotifiers(dispatch) {
go func(n notifier.Bot) {
defer analytics.ReportPanicIfOccurs(d.log, d.reporter)
err := n.SendMessage(ctx, genericMsg, sources)
if err != nil {
d.log.Errorf("while sending action result message: %s", err.Error())
}
}(n)
}
}
}
type genericNotifier interface {
IntegrationName() config.CommPlatformIntegration
Type() config.IntegrationType
}
func (d *Dispatcher) reportSuccess(ctx context.Context, n genericNotifier, pluginName string, event source.Event, sourceName string) error {
errs := multierror.New()
reportErr := d.reporter.ReportHandledEventSuccess(analytics.ReportEvent{
IntegrationType: n.Type(),
Platform: n.IntegrationName(),
PluginName: pluginName,
AnonymizedEventFields: event.AnalyticsLabels,
})
if reportErr != nil {
errs = multierror.Append(errs, fmt.Errorf("while reporting %s analytics: %w", n.Type(), reportErr))
}
if err := d.reportAudit(ctx, pluginName, fmt.Sprintf("%v", event.RawObject), sourceName); err != nil {
errs = multierror.Append(errs, fmt.Errorf("while reporting %s audit event: %w", n.Type(), reportErr))
}
return errs.ErrorOrNil()
}
func (d *Dispatcher) reportError(ctx context.Context, err error, n genericNotifier, pluginName string, event source.Event, sourceName string) error {
errs := multierror.New()
reportErr := d.reporter.ReportHandledEventError(analytics.ReportEvent{
IntegrationType: n.Type(),
Platform: n.IntegrationName(),
PluginName: pluginName,
AnonymizedEventFields: event.AnalyticsLabels,
}, err)
if reportErr != nil {
errs = multierror.Append(errs, fmt.Errorf("while reporting %s analytics: %w", n.Type(), reportErr))
}
// TODO: add additional metadata about event failed to send
if err := d.reportAudit(ctx, pluginName, fmt.Sprintf("%v", event.RawObject), sourceName); err != nil {
errs = multierror.Append(errs, fmt.Errorf("while reporting %s audit event: %w", n.Type(), reportErr))
}
return nil
}
@pkosiec addressed all suggestions and created a ticket for audit event metadata https://github.com/kubeshop/botkube-cloud/issues/203 |
Description
Changes proposed in this pull request:
api.Message
Testing
Build plugins
Run plugin server
Run Botkube with the following config
ℹ️ You can use Webhook Site Tester for webhook URL
Create a pod to see a notification
Related issue(s)
Fixes #999