From 14dc9341e5ae9da1c46d8e1c1aae417d22fa5a40 Mon Sep 17 00:00:00 2001 From: Mateusz Szostok Date: Tue, 20 Sep 2022 15:25:53 +0200 Subject: [PATCH] Persist sent help messages (#738) --- cmd/botkube/main.go | 68 +++++----- helm/botkube/README.md | 83 ++++++------ helm/botkube/templates/botkube-cm.yaml | 9 ++ helm/botkube/templates/deployment.yaml | 2 + helm/botkube/templates/systemroles.yaml | 33 +++++ helm/botkube/values.yaml | 4 + internal/storage/help.go | 123 ++++++++++++++++++ pkg/config/config.go | 18 ++- pkg/config/default.yaml | 4 + .../TestLoadConfigSuccess/config.golden.yaml | 3 + pkg/execute/notifier_test.go | 1 + pkg/sources/sources.go | 12 ++ 12 files changed, 277 insertions(+), 83 deletions(-) create mode 100644 helm/botkube/templates/botkube-cm.yaml create mode 100644 helm/botkube/templates/systemroles.yaml create mode 100644 internal/storage/help.go diff --git a/cmd/botkube/main.go b/cmd/botkube/main.go index 70af1a68c..d2abfb565 100644 --- a/cmd/botkube/main.go +++ b/cmd/botkube/main.go @@ -27,6 +27,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/manager/signals" "github.com/kubeshop/botkube/internal/analytics" + "github.com/kubeshop/botkube/internal/storage" "github.com/kubeshop/botkube/pkg/bot" "github.com/kubeshop/botkube/pkg/bot/interactive" "github.com/kubeshop/botkube/pkg/config" @@ -159,7 +160,7 @@ func run() error { commCfg := conf.Communications var ( notifiers []controller.Notifier - bots []bot.Bot + bots = map[string]bot.Bot{} ) // TODO: Current limitation: Communication platform config should be separate inside every group: @@ -169,14 +170,16 @@ func run() error { for commGroupName, commGroupCfg := range commCfg { commGroupLogger := logger.WithField(commGroupFieldKey, commGroupName) - router.AddAnyBindingsByName(commGroupCfg.Slack.Channels) - router.AddAnyBindingsByName(commGroupCfg.Mattermost.Channels) - router.AddAnyBindings(commGroupCfg.Teams.Bindings) - router.AddAnyBindingsByID(commGroupCfg.Discord.Channels) - for _, index := range commGroupCfg.Elasticsearch.Indices { - router.AddAnySinkBindings(index.Bindings) + router.AddCommunicationsBindings(commGroupCfg) + + scheduleBot := func(in bot.Bot) { + notifiers = append(notifiers, in) + bots[fmt.Sprintf("%s-%s", commGroupName, in.IntegrationName())] = in + errGroup.Go(func() error { + defer analytics.ReportPanicIfOccurs(commGroupLogger, reporter) + return in.Start(ctx) + }) } - router.AddAnySinkBindings(commGroupCfg.Webhook.Bindings) // Run bots if commGroupCfg.Slack.Enabled { @@ -184,12 +187,7 @@ func run() error { if err != nil { return reportFatalError("while creating Slack bot", err) } - notifiers = append(notifiers, sb) - bots = append(bots, sb) - errGroup.Go(func() error { - defer analytics.ReportPanicIfOccurs(commGroupLogger, reporter) - return sb.Start(ctx) - }) + scheduleBot(sb) } if commGroupCfg.Mattermost.Enabled { @@ -197,12 +195,7 @@ func run() error { if err != nil { return reportFatalError("while creating Mattermost bot", err) } - notifiers = append(notifiers, mb) - bots = append(bots, mb) - errGroup.Go(func() error { - defer analytics.ReportPanicIfOccurs(commGroupLogger, reporter) - return mb.Start(ctx) - }) + scheduleBot(mb) } if commGroupCfg.Teams.Enabled { @@ -210,12 +203,7 @@ func run() error { if err != nil { return reportFatalError("while creating Teams bot", err) } - notifiers = append(notifiers, tb) - bots = append(bots, tb) - errGroup.Go(func() error { - defer analytics.ReportPanicIfOccurs(commGroupLogger, reporter) - return tb.Start(ctx) - }) + scheduleBot(tb) } if commGroupCfg.Discord.Enabled { @@ -223,12 +211,7 @@ func run() error { if err != nil { return reportFatalError("while creating Discord bot", err) } - notifiers = append(notifiers, db) - bots = append(bots, db) - errGroup.Go(func() error { - defer analytics.ReportPanicIfOccurs(commGroupLogger, reporter) - return db.Start(ctx) - }) + scheduleBot(db) } // Run sinks @@ -251,7 +234,8 @@ func run() error { } // Send help message - err = sendHelp(ctx, conf.Settings.ClusterName, bots) + helpDB := storage.NewForHelp(conf.Settings.SystemConfigMap.Namespace, conf.Settings.SystemConfigMap.Name, k8sCli) + err = sendHelp(ctx, helpDB, conf.Settings.ClusterName, bots) if err != nil { return fmt.Errorf("while sending initial help message: %w", err) } @@ -397,14 +381,26 @@ func reportFatalErrFn(logger logrus.FieldLogger, reporter analytics.Reporter) fu } // sendHelp sends the help message to all interactive bots. -func sendHelp(ctx context.Context, clusterName string, notifiers []bot.Bot) error { - for _, notifier := range notifiers { +func sendHelp(ctx context.Context, s *storage.Help, clusterName string, notifiers map[string]bot.Bot) error { + alreadySentHelp, err := s.GetSentHelpDetails(ctx) + if err != nil { + return fmt.Errorf("while getting the help data: %w", err) + } + + var sent []string + + for key, notifier := range notifiers { + if alreadySentHelp[key] { + continue + } + help := interactive.Help(notifier.IntegrationName(), clusterName, notifier.BotName()) err := notifier.SendMessage(ctx, help) if err != nil { return fmt.Errorf("while sending help message for %s: %w", notifier.IntegrationName(), err) } + sent = append(sent, key) } - return nil + return s.MarkHelpAsSent(ctx, sent) } diff --git a/helm/botkube/README.md b/helm/botkube/README.md index a4eb4f578..656b41c1f 100644 --- a/helm/botkube/README.md +++ b/helm/botkube/README.md @@ -111,47 +111,48 @@ Controller for the BotKube Slack app which helps you monitor your Kubernetes clu | [settings.upgradeNotifier](./values.yaml#L458) | bool | `true` | If true, notifies about new BotKube releases. | | [settings.log.level](./values.yaml#L462) | string | `"info"` | Sets one of the log levels. Allowed values: `info`, `warn`, `debug`, `error`, `fatal`, `panic`. | | [settings.log.disableColors](./values.yaml#L464) | bool | `false` | If true, disable ANSI colors in logging. | -| [ssl.enabled](./values.yaml#L469) | bool | `false` | If true, specify cert path in `config.ssl.cert` property or K8s Secret in `config.ssl.existingSecretName`. | -| [ssl.existingSecretName](./values.yaml#L475) | string | `""` | Using existing SSL Secret. It MUST be in `botkube` Namespace. | -| [ssl.cert](./values.yaml#L478) | string | `""` | SSL Certificate file e.g certs/my-cert.crt. | -| [service](./values.yaml#L481) | object | `{"name":"metrics","port":2112,"targetPort":2112}` | Configures Service settings for ServiceMonitor CR. | -| [ingress](./values.yaml#L488) | object | `{"annotations":{"kubernetes.io/ingress.class":"nginx"},"create":false,"host":"HOST","tls":{"enabled":false,"secretName":""}}` | Configures Ingress settings that exposes MS Teams endpoint. [Ref doc](https://kubernetes.io/docs/concepts/services-networking/ingress/#the-ingress-resource). | -| [serviceMonitor](./values.yaml#L499) | object | `{"enabled":false,"interval":"10s","labels":{},"path":"/metrics","port":"metrics"}` | Configures ServiceMonitor settings. [Ref doc](https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#servicemonitor). | -| [deployment.annotations](./values.yaml#L509) | object | `{}` | Extra annotations to pass to the BotKube Deployment. | -| [extraAnnotations](./values.yaml#L516) | object | `{}` | Extra annotations to pass to the BotKube Pod. | -| [extraLabels](./values.yaml#L518) | object | `{}` | Extra labels to pass to the BotKube Pod. | -| [priorityClassName](./values.yaml#L520) | string | `""` | Priority class name for the BotKube Pod. | -| [nameOverride](./values.yaml#L523) | string | `""` | Fully override "botkube.name" template. | -| [fullnameOverride](./values.yaml#L525) | string | `""` | Fully override "botkube.fullname" template. | -| [resources](./values.yaml#L531) | object | `{}` | The BotKube Pod resource request and limits. We usually recommend not to specify default resources and to leave this as a conscious choice for the user. This also increases chances charts run on environments with little resources, such as Minikube. [Ref docs](https://kubernetes.io/docs/user-guide/compute-resources/) | -| [extraEnv](./values.yaml#L543) | list | `[]` | Extra environment variables to pass to the BotKube container. [Ref docs](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#environment-variables). | -| [extraVolumes](./values.yaml#L555) | list | `[]` | Extra volumes to pass to the BotKube container. Mount it later with extraVolumeMounts. [Ref docs](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/volume/#Volume). | -| [extraVolumeMounts](./values.yaml#L570) | list | `[]` | Extra volume mounts to pass to the BotKube container. [Ref docs](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#volumes-1). | -| [nodeSelector](./values.yaml#L588) | object | `{}` | Node labels for BotKube Pod assignment. [Ref doc](https://kubernetes.io/docs/user-guide/node-selection/). | -| [tolerations](./values.yaml#L592) | list | `[]` | Tolerations for BotKube Pod assignment. [Ref doc](https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/). | -| [affinity](./values.yaml#L596) | object | `{}` | Affinity for BotKube Pod assignment. [Ref doc](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity). | -| [rbac](./values.yaml#L600) | object | `{"create":true,"rules":[{"apiGroups":["*"],"resources":["*"],"verbs":["get","watch","list"]}]}` | Role Based Access for BotKube Pod. [Ref doc](https://kubernetes.io/docs/admin/authorization/rbac/). | -| [serviceAccount.create](./values.yaml#L609) | bool | `true` | If true, a ServiceAccount is automatically created. | -| [serviceAccount.name](./values.yaml#L612) | string | `""` | The name of the service account to use. If not set, a name is generated using the fullname template. | -| [serviceAccount.annotations](./values.yaml#L614) | object | `{}` | Extra annotations for the ServiceAccount. | -| [extraObjects](./values.yaml#L617) | list | `[]` | Extra Kubernetes resources to create. Helm templating is allowed as it is evaluated before creating the resources. | -| [analytics.disable](./values.yaml#L645) | bool | `false` | If true, sending anonymous analytics is disabled. To learn what date we collect, see [Privacy Policy](https://botkube.io/privacy#privacy-policy). | -| [e2eTest.image.registry](./values.yaml#L651) | string | `"ghcr.io"` | Test runner image registry. | -| [e2eTest.image.repository](./values.yaml#L653) | string | `"kubeshop/botkube-test"` | Test runner image repository. | -| [e2eTest.image.pullPolicy](./values.yaml#L655) | string | `"IfNotPresent"` | Test runner image pull policy. | -| [e2eTest.image.tag](./values.yaml#L657) | string | `"v9.99.9-dev"` | Test runner image tag. Default tag is `appVersion` from Chart.yaml. | -| [e2eTest.deployment](./values.yaml#L659) | object | `{"waitTimeout":"3m"}` | Configures BotKube Deployment related data. | -| [e2eTest.slack.botName](./values.yaml#L664) | string | `"botkube"` | Name of the BotKube bot to interact with during the e2e tests. | -| [e2eTest.slack.testerName](./values.yaml#L666) | string | `"botkube_tester"` | Name of the BotKube Tester bot that sends messages during the e2e tests. | -| [e2eTest.slack.testerAppToken](./values.yaml#L668) | string | `""` | Slack tester application token that interacts with BotKube bot. | -| [e2eTest.slack.additionalContextMessage](./values.yaml#L670) | string | `""` | Additional message that is sent by Tester. You can pass e.g. pull request number or source link where these tests are run from. | -| [e2eTest.slack.messageWaitTimeout](./values.yaml#L672) | string | `"1m"` | Message wait timeout. It defines how long we wait to ensure that notification were not sent when disabled. | -| [e2eTest.discord.botName](./values.yaml#L675) | string | `"botkube"` | Name of the BotKube bot to interact with during the e2e tests. | -| [e2eTest.discord.testerName](./values.yaml#L677) | string | `"botkube_tester"` | Name of the BotKube Tester bot that sends messages during the e2e tests. | -| [e2eTest.discord.guildID](./values.yaml#L679) | string | `""` | Discord Guild ID (discord server ID) used to run e2e tests | -| [e2eTest.discord.testerAppToken](./values.yaml#L681) | string | `""` | Discord tester application token that interacts with BotKube bot. | -| [e2eTest.discord.additionalContextMessage](./values.yaml#L683) | string | `""` | Additional message that is sent by Tester. You can pass e.g. pull request number or source link where these tests are run from. | -| [e2eTest.discord.messageWaitTimeout](./values.yaml#L685) | string | `"1m"` | Message wait timeout. It defines how long we wait to ensure that notification were not sent when disabled. | +| [settings.systemConfigMap](./values.yaml#L467) | object | `{"name":"botkube-system"}` | BotKube's system ConfigMap where internal data is stored. | +| [ssl.enabled](./values.yaml#L473) | bool | `false` | If true, specify cert path in `config.ssl.cert` property or K8s Secret in `config.ssl.existingSecretName`. | +| [ssl.existingSecretName](./values.yaml#L479) | string | `""` | Using existing SSL Secret. It MUST be in `botkube` Namespace. | +| [ssl.cert](./values.yaml#L482) | string | `""` | SSL Certificate file e.g certs/my-cert.crt. | +| [service](./values.yaml#L485) | object | `{"name":"metrics","port":2112,"targetPort":2112}` | Configures Service settings for ServiceMonitor CR. | +| [ingress](./values.yaml#L492) | object | `{"annotations":{"kubernetes.io/ingress.class":"nginx"},"create":false,"host":"HOST","tls":{"enabled":false,"secretName":""}}` | Configures Ingress settings that exposes MS Teams endpoint. [Ref doc](https://kubernetes.io/docs/concepts/services-networking/ingress/#the-ingress-resource). | +| [serviceMonitor](./values.yaml#L503) | object | `{"enabled":false,"interval":"10s","labels":{},"path":"/metrics","port":"metrics"}` | Configures ServiceMonitor settings. [Ref doc](https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#servicemonitor). | +| [deployment.annotations](./values.yaml#L513) | object | `{}` | Extra annotations to pass to the BotKube Deployment. | +| [extraAnnotations](./values.yaml#L520) | object | `{}` | Extra annotations to pass to the BotKube Pod. | +| [extraLabels](./values.yaml#L522) | object | `{}` | Extra labels to pass to the BotKube Pod. | +| [priorityClassName](./values.yaml#L524) | string | `""` | Priority class name for the BotKube Pod. | +| [nameOverride](./values.yaml#L527) | string | `""` | Fully override "botkube.name" template. | +| [fullnameOverride](./values.yaml#L529) | string | `""` | Fully override "botkube.fullname" template. | +| [resources](./values.yaml#L535) | object | `{}` | The BotKube Pod resource request and limits. We usually recommend not to specify default resources and to leave this as a conscious choice for the user. This also increases chances charts run on environments with little resources, such as Minikube. [Ref docs](https://kubernetes.io/docs/user-guide/compute-resources/) | +| [extraEnv](./values.yaml#L547) | list | `[]` | Extra environment variables to pass to the BotKube container. [Ref docs](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#environment-variables). | +| [extraVolumes](./values.yaml#L559) | list | `[]` | Extra volumes to pass to the BotKube container. Mount it later with extraVolumeMounts. [Ref docs](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/volume/#Volume). | +| [extraVolumeMounts](./values.yaml#L574) | list | `[]` | Extra volume mounts to pass to the BotKube container. [Ref docs](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#volumes-1). | +| [nodeSelector](./values.yaml#L592) | object | `{}` | Node labels for BotKube Pod assignment. [Ref doc](https://kubernetes.io/docs/user-guide/node-selection/). | +| [tolerations](./values.yaml#L596) | list | `[]` | Tolerations for BotKube Pod assignment. [Ref doc](https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/). | +| [affinity](./values.yaml#L600) | object | `{}` | Affinity for BotKube Pod assignment. [Ref doc](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity). | +| [rbac](./values.yaml#L604) | object | `{"create":true,"rules":[{"apiGroups":["*"],"resources":["*"],"verbs":["get","watch","list"]}]}` | Role Based Access for BotKube Pod. [Ref doc](https://kubernetes.io/docs/admin/authorization/rbac/). | +| [serviceAccount.create](./values.yaml#L613) | bool | `true` | If true, a ServiceAccount is automatically created. | +| [serviceAccount.name](./values.yaml#L616) | string | `""` | The name of the service account to use. If not set, a name is generated using the fullname template. | +| [serviceAccount.annotations](./values.yaml#L618) | object | `{}` | Extra annotations for the ServiceAccount. | +| [extraObjects](./values.yaml#L621) | list | `[]` | Extra Kubernetes resources to create. Helm templating is allowed as it is evaluated before creating the resources. | +| [analytics.disable](./values.yaml#L649) | bool | `false` | If true, sending anonymous analytics is disabled. To learn what date we collect, see [Privacy Policy](https://botkube.io/privacy#privacy-policy). | +| [e2eTest.image.registry](./values.yaml#L655) | string | `"ghcr.io"` | Test runner image registry. | +| [e2eTest.image.repository](./values.yaml#L657) | string | `"kubeshop/botkube-test"` | Test runner image repository. | +| [e2eTest.image.pullPolicy](./values.yaml#L659) | string | `"IfNotPresent"` | Test runner image pull policy. | +| [e2eTest.image.tag](./values.yaml#L661) | string | `"v9.99.9-dev"` | Test runner image tag. Default tag is `appVersion` from Chart.yaml. | +| [e2eTest.deployment](./values.yaml#L663) | object | `{"waitTimeout":"3m"}` | Configures BotKube Deployment related data. | +| [e2eTest.slack.botName](./values.yaml#L668) | string | `"botkube"` | Name of the BotKube bot to interact with during the e2e tests. | +| [e2eTest.slack.testerName](./values.yaml#L670) | string | `"botkube_tester"` | Name of the BotKube Tester bot that sends messages during the e2e tests. | +| [e2eTest.slack.testerAppToken](./values.yaml#L672) | string | `""` | Slack tester application token that interacts with BotKube bot. | +| [e2eTest.slack.additionalContextMessage](./values.yaml#L674) | string | `""` | Additional message that is sent by Tester. You can pass e.g. pull request number or source link where these tests are run from. | +| [e2eTest.slack.messageWaitTimeout](./values.yaml#L676) | string | `"1m"` | Message wait timeout. It defines how long we wait to ensure that notification were not sent when disabled. | +| [e2eTest.discord.botName](./values.yaml#L679) | string | `"botkube"` | Name of the BotKube bot to interact with during the e2e tests. | +| [e2eTest.discord.testerName](./values.yaml#L681) | string | `"botkube_tester"` | Name of the BotKube Tester bot that sends messages during the e2e tests. | +| [e2eTest.discord.guildID](./values.yaml#L683) | string | `""` | Discord Guild ID (discord server ID) used to run e2e tests | +| [e2eTest.discord.testerAppToken](./values.yaml#L685) | string | `""` | Discord tester application token that interacts with BotKube bot. | +| [e2eTest.discord.additionalContextMessage](./values.yaml#L687) | string | `""` | Additional message that is sent by Tester. You can pass e.g. pull request number or source link where these tests are run from. | +| [e2eTest.discord.messageWaitTimeout](./values.yaml#L689) | string | `"1m"` | Message wait timeout. It defines how long we wait to ensure that notification were not sent when disabled. | ### AWS IRSA on EKS support diff --git a/helm/botkube/templates/botkube-cm.yaml b/helm/botkube/templates/botkube-cm.yaml new file mode 100644 index 000000000..23cd3e24d --- /dev/null +++ b/helm/botkube/templates/botkube-cm.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Values.settings.systemConfigMap.name }} + labels: + app.kubernetes.io/name: {{ include "botkube.name" . }} + helm.sh/chart: {{ include "botkube.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} diff --git a/helm/botkube/templates/deployment.yaml b/helm/botkube/templates/deployment.yaml index 8bd156971..06326b8d7 100644 --- a/helm/botkube/templates/deployment.yaml +++ b/helm/botkube/templates/deployment.yaml @@ -77,6 +77,8 @@ spec: - name: BOTKUBE_SETTINGS_KUBECONFIG value: "/.kube/config" {{- end }} + - name: BOTKUBE_SETTINGS_SYSTEM__CONFIG__MAP_NAMESPACE + value: "{{.Release.Namespace}}" {{- with .Values.extraEnv }} {{ toYaml . | nindent 12 }} {{- end }} diff --git a/helm/botkube/templates/systemroles.yaml b/helm/botkube/templates/systemroles.yaml new file mode 100644 index 000000000..165e76a49 --- /dev/null +++ b/helm/botkube/templates/systemroles.yaml @@ -0,0 +1,33 @@ +{{- if .Values.rbac.create }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "botkube.fullname" . }}-system + labels: + app.kubernetes.io/name: {{ include "botkube.name" . }} + helm.sh/chart: {{ include "botkube.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +rules: + - apiGroups: [""] + resources: ["configmaps"] + verbs: ["update", "get", "create"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "botkube.fullname" . }}-system + labels: + app.kubernetes.io/name: {{ include "botkube.name" . }} + helm.sh/chart: {{ include "botkube.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "botkube.fullname" . }}-system +subjects: +- kind: ServiceAccount + name: {{ include "botkube.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{ end }} diff --git a/helm/botkube/values.yaml b/helm/botkube/values.yaml index c35ae1974..9633fd0c9 100644 --- a/helm/botkube/values.yaml +++ b/helm/botkube/values.yaml @@ -463,6 +463,10 @@ settings: # -- If true, disable ANSI colors in logging. disableColors: false + # -- BotKube's system ConfigMap where internal data is stored. + systemConfigMap: + name: botkube-system + ## For using custom SSL certificates. ssl: # -- If true, specify cert path in `config.ssl.cert` property or K8s Secret in `config.ssl.existingSecretName`. diff --git a/internal/storage/help.go b/internal/storage/help.go new file mode 100644 index 000000000..23871e489 --- /dev/null +++ b/internal/storage/help.go @@ -0,0 +1,123 @@ +package storage + +import ( + "context" + "encoding/json" + "fmt" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +// HelpEntries defines the help persistence model. +type HelpEntries map[string]bool + +const helpKey = "help-message" + +// Help provides functionality to persist the information about sent help messages. +type Help struct { + systemConfigMapName string + systemConfigMapNamespace string + + k8sCli kubernetes.Interface +} + +// NewForHelp returns a new Help instance. +func NewForHelp(ns, name string, k8sCli kubernetes.Interface) *Help { + return &Help{ + systemConfigMapNamespace: ns, + systemConfigMapName: name, + k8sCli: k8sCli, + } +} + +// GetSentHelpDetails returns details about sent help messages. +func (a *Help) GetSentHelpDetails(ctx context.Context) (HelpEntries, error) { + obj, err := a.k8sCli.CoreV1().ConfigMaps(a.systemConfigMapNamespace).Get(ctx, a.systemConfigMapName, metav1.GetOptions{}) + switch { + case err == nil: + case apierrors.IsNotFound(err): + return HelpEntries{}, nil + default: + return HelpEntries{}, fmt.Errorf("while getting the Config Map: %w", err) + } + + return a.extractHelpDetails(obj) +} + +// MarkHelpAsSent marks a given sent keys as sent. +func (a *Help) MarkHelpAsSent(ctx context.Context, sent []string) error { + alreadySent := HelpEntries{} + for _, item := range sent { + alreadySent[item] = true + } + rawSent, err := json.Marshal(alreadySent) + if err != nil { + return fmt.Errorf("while marshaling input keys: %w", err) + } + + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: a.systemConfigMapName, + Namespace: a.systemConfigMapNamespace, + }, + Data: map[string]string{ + helpKey: string(rawSent), + }, + } + + _, err = a.k8sCli.CoreV1().ConfigMaps(a.systemConfigMapNamespace).Create(ctx, cm, metav1.CreateOptions{}) + switch { + case err == nil: + case apierrors.IsAlreadyExists(err): + old, err := a.k8sCli.CoreV1().ConfigMaps(cm.Namespace).Get(ctx, cm.Name, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("while getting already existing ConfigMap: %w", err) + } + + previousEntries, err := a.extractHelpDetails(old) + if err != nil { + return fmt.Errorf("while extracting help details: %w", err) + } + + for _, item := range sent { + previousEntries[item] = true + } + + newRawSent, err := json.Marshal(previousEntries) + if err != nil { + return fmt.Errorf("while marshaling final output: %w", err) + } + + newCM := old.DeepCopy() + if newCM.Data == nil { + newCM.Data = map[string]string{} + } + newCM.Data[helpKey] = string(newRawSent) + + _, err = a.k8sCli.CoreV1().ConfigMaps(cm.Namespace).Update(ctx, newCM, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("while updating the ConfigMap with help details: %w", err) + } + + default: + return fmt.Errorf("while creating the ConfigMap with help details: %w", err) + } + + return nil +} + +func (a *Help) extractHelpDetails(cm *corev1.ConfigMap) (HelpEntries, error) { + data, found := cm.Data[helpKey] + if !found { + return HelpEntries{}, nil + } + + out := HelpEntries{} + if err := json.Unmarshal([]byte(data), &out); err != nil { + return HelpEntries{}, fmt.Errorf("while unmarhshaling the help data: %w", err) + } + return out, nil +} diff --git a/pkg/config/config.go b/pkg/config/config.go index c0ed9afac..c320a47ab 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -466,12 +466,12 @@ type Commands struct { // Settings contains BotKube's related configuration. type Settings struct { - ClusterName string `yaml:"clusterName"` - ConfigWatcher bool `yaml:"configWatcher"` - UpgradeNotifier bool `yaml:"upgradeNotifier"` - - MetricsPort string `yaml:"metricsPort"` - Log struct { + ClusterName string `yaml:"clusterName"` + ConfigWatcher bool `yaml:"configWatcher"` + UpgradeNotifier bool `yaml:"upgradeNotifier"` + SystemConfigMap SystemConfigMap `yaml:"systemConfigMap"` + MetricsPort string `yaml:"metricsPort"` + Log struct { Level string `yaml:"level"` DisableColors bool `yaml:"disableColors"` } `yaml:"log"` @@ -479,6 +479,12 @@ type Settings struct { Kubeconfig string `yaml:"kubeconfig"` } +// SystemConfigMap holds the configuration for BotKube system config map "storage". +type SystemConfigMap struct { + Name string `yaml:"name,omitempty"` + Namespace string `yaml:"namespace,omitempty"` +} + func (eventType EventType) String() string { return string(eventType) } diff --git a/pkg/config/default.yaml b/pkg/config/default.yaml index aa9836b08..79898ab9d 100644 --- a/pkg/config/default.yaml +++ b/pkg/config/default.yaml @@ -5,5 +5,9 @@ settings: disableColors: "false" informersResyncPeriod: "30m" + systemConfigMap: + name: botkube-system + namespace: botkube + analytics: disable: false diff --git a/pkg/config/testdata/TestLoadConfigSuccess/config.golden.yaml b/pkg/config/testdata/TestLoadConfigSuccess/config.golden.yaml index a5b993190..fd21a2eac 100644 --- a/pkg/config/testdata/TestLoadConfigSuccess/config.golden.yaml +++ b/pkg/config/testdata/TestLoadConfigSuccess/config.golden.yaml @@ -376,6 +376,9 @@ settings: clusterName: cluster-name-from-env configWatcher: true upgradeNotifier: true + systemConfigMap: + name: botkube-system + namespace: botkube metricsPort: "1313" log: level: error diff --git a/pkg/execute/notifier_test.go b/pkg/execute/notifier_test.go index 60213748b..97e495f8f 100644 --- a/pkg/execute/notifier_test.go +++ b/pkg/execute/notifier_test.go @@ -95,6 +95,7 @@ func TestNotifierExecutor_Do_Success(t *testing.T) { clusterName: foo configWatcher: false upgradeNotifier: false + systemConfigMap: {} metricsPort: "" log: level: "" diff --git a/pkg/sources/sources.go b/pkg/sources/sources.go index c0700a786..e043e86d5 100644 --- a/pkg/sources/sources.go +++ b/pkg/sources/sources.go @@ -56,6 +56,18 @@ func NewRouter(mapper meta.RESTMapper, dynamicCli dynamic.Interface, log logrus. } } +// AddCommunicationsBindings adds source binding from a given communications +func (r *Router) AddCommunicationsBindings(c config.Communications) { + r.AddAnyBindingsByName(c.Slack.Channels) + r.AddAnyBindingsByName(c.Mattermost.Channels) + r.AddAnyBindings(c.Teams.Bindings) + r.AddAnyBindingsByID(c.Discord.Channels) + for _, index := range c.Elasticsearch.Indices { + r.AddAnySinkBindings(index.Bindings) + } + r.AddAnySinkBindings(c.Webhook.Bindings) +} + // AddAnyBindingsByName adds source binding names // to dictate which source bindings the router should use. func (r *Router) AddAnyBindingsByName(c config.IdentifiableMap[config.ChannelBindingsByName]) *Router {