diff --git a/cmd/botkube/main.go b/cmd/botkube/main.go index eaf975613..ea4fc2c52 100644 --- a/cmd/botkube/main.go +++ b/cmd/botkube/main.go @@ -16,6 +16,7 @@ import ( "github.com/spf13/pflag" "golang.org/x/sync/errgroup" "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/discovery" cacheddiscovery "k8s.io/client-go/discovery/cached" "k8s.io/client-go/dynamic" @@ -27,6 +28,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/manager/signals" "github.com/kubeshop/botkube/internal/analytics" + "github.com/kubeshop/botkube/internal/lifecycle" "github.com/kubeshop/botkube/internal/storage" "github.com/kubeshop/botkube/pkg/bot" "github.com/kubeshop/botkube/pkg/bot/interactive" @@ -36,6 +38,7 @@ import ( "github.com/kubeshop/botkube/pkg/execute/kubectl" "github.com/kubeshop/botkube/pkg/filterengine" "github.com/kubeshop/botkube/pkg/httpsrv" + "github.com/kubeshop/botkube/pkg/notifier" "github.com/kubeshop/botkube/pkg/recommendation" "github.com/kubeshop/botkube/pkg/sink" "github.com/kubeshop/botkube/pkg/sources" @@ -159,7 +162,7 @@ func run() error { commCfg := conf.Communications var ( - notifiers []controller.Notifier + notifiers []notifier.Notifier bots = map[string]bot.Bot{} ) @@ -241,6 +244,39 @@ func run() error { } } + // Lifecycle server + if conf.Settings.LifecycleServer.Enabled { + lifecycleSrv := lifecycle.NewServer( + logger.WithField(componentLogFieldKey, "Lifecycle server"), + k8sCli, + conf.Settings.LifecycleServer, + conf.Settings.ClusterName, + func(msg string) error { + return notifier.SendPlaintextMessage(ctx, notifiers, msg) + }, + ) + errGroup.Go(func() error { + defer analytics.ReportPanicIfOccurs(logger, reporter) + return lifecycleSrv.Serve(ctx) + }) + } + + if conf.ConfigWatcher.Enabled { + err := config.WaitForWatcherSync( + ctx, + logger.WithField(componentLogFieldKey, "Config Watcher Sync"), + conf.ConfigWatcher, + ) + if err != nil { + if err != wait.ErrWaitTimeout { + return reportFatalError("while waiting for Config Watcher sync", err) + } + + // non-blocking error, move forward + logger.Warn("Config Watcher is still not synchronized. Read the logs of the sidecar container to see the cause. Continuing running BotKube...") + } + } + // Send help message helpDB := storage.NewForHelp(conf.Settings.SystemConfigMap.Namespace, conf.Settings.SystemConfigMap.Name, k8sCli) err = sendHelp(ctx, helpDB, conf.Settings.ClusterName, bots) @@ -264,20 +300,6 @@ func run() error { }) } - // Start Config Watcher - if conf.Settings.ConfigWatcher { - cfgWatcher := controller.NewConfigWatcher( - logger.WithField(componentLogFieldKey, "Config Watcher"), - confDetails.CfgFilesToWatch, - conf.Settings.ClusterName, - notifiers, - ) - errGroup.Go(func() error { - defer analytics.ReportPanicIfOccurs(logger, reporter) - return cfgWatcher.Do(ctx, cancelCtxFn) - }) - } - recommFactory := recommendation.NewFactory(logger.WithField(componentLogFieldKey, "Recommendations"), dynamicCli) // Create and start controller diff --git a/global_config.yaml.tpl b/global_config.yaml.tpl index 9614ad82d..465ab3f2e 100644 --- a/global_config.yaml.tpl +++ b/global_config.yaml.tpl @@ -238,10 +238,19 @@ settings: # Cluster name to differentiate incoming messages clusterName: not-configured # Set true to enable config watcher - configWatcher: true + # Server configuration which exposes functionality related to the app lifecycle. + lifecycleServer: + deployment: + name: botkube + namespace: botkube + port: "2113" # Set false to disable upgrade notification upgradeNotifier: true +# Parameters for the config watcher container. +configWatcher: + enabled: false # Used only on Kubernetes + # Map of enabled executors. The `executors` property name is an alias for a given configuration. # It's used as a binding reference. # diff --git a/go.mod b/go.mod index 92b966b53..fd87d976d 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ require ( github.com/aws/aws-sdk-go v1.44.20 github.com/bwmarrin/discordgo v0.25.0 github.com/dustin/go-humanize v1.0.0 - github.com/fsnotify/fsnotify v1.5.4 github.com/go-playground/locales v0.14.0 github.com/go-playground/universal-translator v0.18.0 github.com/go-playground/validator/v10 v10.11.0 @@ -54,6 +53,7 @@ require ( github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect github.com/francoispqt/gojay v1.2.13 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/fvbommel/sortorder v1.0.1 // indirect github.com/go-asn1-ber/asn1-ber v1.5.3 // indirect github.com/go-errors/errors v1.0.1 // indirect diff --git a/hack/goreleaser.sh b/hack/goreleaser.sh index 7534ac39a..a23ee87a5 100755 --- a/hack/goreleaser.sh +++ b/hack/goreleaser.sh @@ -123,15 +123,15 @@ build() { } build_single() { - export GORELEASER_CURRENT_TAG=v9.99.9-dev + export IMAGE_TAG=v9.99.9-dev docker run --rm --privileged \ -v "$PWD":/go/src/github.com/kubeshop/botkube \ -v /var/run/docker.sock:/var/run/docker.sock \ -w /go/src/github.com/kubeshop/botkube \ - -e GORELEASER_CURRENT_TAG=${GORELEASER_CURRENT_TAG} \ + -e IMAGE_TAG=${IMAGE_TAG} \ -e ANALYTICS_API_KEY="${ANALYTICS_API_KEY}" \ goreleaser/goreleaser build --single-target --rm-dist --snapshot --id botkube -o "./botkube" - docker build -f "$PWD/build/Dockerfile" --platform "${IMAGE_PLATFORM}" -t "${IMAGE_REGISTRY}/${IMAGE_REPOSITORY}:${GORELEASER_CURRENT_TAG}" . + docker build -f "$PWD/build/Dockerfile" --platform "${IMAGE_PLATFORM}" -t "${IMAGE_REGISTRY}/${IMAGE_REPOSITORY}:${IMAGE_TAG}" . rm "$PWD/botkube" } diff --git a/helm/botkube/README.md b/helm/botkube/README.md index 395f2048f..bad3b217f 100644 --- a/helm/botkube/README.md +++ b/helm/botkube/README.md @@ -58,114 +58,121 @@ Controller for the BotKube Slack app which helps you monitor your Kubernetes clu | [executors.kubectl-read-only.kubectl.commands.resources](./values.yaml#L262) | list | `["deployments","pods","namespaces","daemonsets","statefulsets","storageclasses","nodes","configmaps"]` | Configures which K8s resource are allowed. | | [executors.kubectl-read-only.kubectl.defaultNamespace](./values.yaml#L264) | string | `"default"` | Configures the default Namespace for executing BotKube `kubectl` commands. If not set, uses the 'default'. | | [executors.kubectl-read-only.kubectl.restrictAccess](./values.yaml#L266) | bool | `false` | If true, enables commands execution from configured channel only. | -| [existingCommunicationsSecretName](./values.yaml#L276) | string | `""` | Configures existing Secret with communication settings. It MUST be in the `botkube` Namespace. | -| [communications](./values.yaml#L283) | object | See the `values.yaml` file for full object. | Map of communication groups. Communication group contains settings for multiple communication platforms. The property name under `communications` object is an alias for a given configuration group. You can define multiple communication groups with different names. | -| [communications.default-group.slack.enabled](./values.yaml#L288) | bool | `false` | If true, enables Slack bot. | -| [communications.default-group.slack.channels](./values.yaml#L292) | object | `{"default":{"bindings":{"executors":["kubectl-read-only"],"sources":["k8s-err-events","k8s-recommendation-events"]},"name":"SLACK_CHANNEL","notification":{"disabled":false}}}` | Map of configured channels. The property name under `channels` object is an alias for a given configuration. | -| [communications.default-group.slack.channels.default.name](./values.yaml#L295) | string | `"SLACK_CHANNEL"` | Slack channel name without '#' prefix where you have added BotKube and want to receive notifications in. | -| [communications.default-group.slack.channels.default.notification.disabled](./values.yaml#L298) | bool | `false` | If true, the notifications are not sent to the channel. They can be enabled with `@BotKube` command anytime. | -| [communications.default-group.slack.channels.default.bindings.executors](./values.yaml#L301) | list | `["kubectl-read-only"]` | Executors configuration for a given channel. | -| [communications.default-group.slack.channels.default.bindings.sources](./values.yaml#L304) | list | `["k8s-err-events","k8s-recommendation-events"]` | Notification sources configuration for a given channel. | -| [communications.default-group.slack.token](./values.yaml#L308) | string | `""` | Slack token. | -| [communications.default-group.slack.notification.type](./values.yaml#L311) | string | `"short"` | Configures notification type that are sent. Possible values: `short`, `long`. | -| [communications.default-group.socketSlack.enabled](./values.yaml#L316) | bool | `false` | If true, enables Slack bot. | -| [communications.default-group.socketSlack.channels](./values.yaml#L320) | object | `{"default":{"bindings":{"executors":["kubectl-read-only"],"sources":["k8s-err-events","k8s-recommendation-events"]},"name":"SLACK_CHANNEL"}}` | Map of configured channels. The property name under `channels` object is an alias for a given configuration. | -| [communications.default-group.socketSlack.channels.default.name](./values.yaml#L323) | string | `"SLACK_CHANNEL"` | Slack channel name without '#' prefix where you have added BotKube and want to receive notifications in. | -| [communications.default-group.socketSlack.channels.default.bindings.executors](./values.yaml#L326) | list | `["kubectl-read-only"]` | Executors configuration for a given channel. | -| [communications.default-group.socketSlack.channels.default.bindings.sources](./values.yaml#L329) | list | `["k8s-err-events","k8s-recommendation-events"]` | Notification sources configuration for a given channel. | -| [communications.default-group.socketSlack.botToken](./values.yaml#L334) | string | `""` | Slack bot token for your own Slack app. [Ref doc](https://api.slack.com/authentication/token-types). | -| [communications.default-group.socketSlack.appToken](./values.yaml#L337) | string | `""` | Slack app-level token for your own Slack app. [Ref doc](https://api.slack.com/authentication/token-types). | -| [communications.default-group.socketSlack.notification.type](./values.yaml#L340) | string | `"short"` | Configures notification type that are sent. Possible values: `short`, `long`. | -| [communications.default-group.mattermost.enabled](./values.yaml#L344) | bool | `false` | If true, enables Mattermost bot. | -| [communications.default-group.mattermost.botName](./values.yaml#L346) | string | `"BotKube"` | User in Mattermost which belongs the specified Personal Access token. | -| [communications.default-group.mattermost.url](./values.yaml#L348) | string | `"MATTERMOST_SERVER_URL"` | The URL (including http/https schema) where Mattermost is running. e.g https://example.com:9243 | -| [communications.default-group.mattermost.token](./values.yaml#L350) | string | `"MATTERMOST_TOKEN"` | Personal Access token generated by BotKube user. | -| [communications.default-group.mattermost.team](./values.yaml#L352) | string | `"MATTERMOST_TEAM"` | The Mattermost Team name where BotKube is added. | -| [communications.default-group.mattermost.channels](./values.yaml#L356) | object | `{"default":{"bindings":{"executors":["kubectl-read-only"],"sources":["k8s-err-events","k8s-recommendation-events"]},"name":"MATTERMOST_CHANNEL","notification":{"disabled":false}}}` | Map of configured channels. The property name under `channels` object is an alias for a given configuration. | -| [communications.default-group.mattermost.channels.default.name](./values.yaml#L360) | string | `"MATTERMOST_CHANNEL"` | The Mattermost channel name for receiving BotKube alerts. The BotKube user needs to be added to it. | -| [communications.default-group.mattermost.channels.default.notification.disabled](./values.yaml#L363) | bool | `false` | If true, the notifications are not sent to the channel. They can be enabled with `@BotKube` command anytime. | -| [communications.default-group.mattermost.channels.default.bindings.executors](./values.yaml#L366) | list | `["kubectl-read-only"]` | Executors configuration for a given channel. | -| [communications.default-group.mattermost.channels.default.bindings.sources](./values.yaml#L369) | list | `["k8s-err-events","k8s-recommendation-events"]` | Notification sources configuration for a given channel. | -| [communications.default-group.mattermost.notification.type](./values.yaml#L374) | string | `"short"` | Configures notification type that are sent. Possible values: `short`, `long`. | -| [communications.default-group.teams.enabled](./values.yaml#L379) | bool | `false` | If true, enables MS Teams bot. | -| [communications.default-group.teams.botName](./values.yaml#L381) | string | `"BotKube"` | The Bot name set while registering Bot to MS Teams. | -| [communications.default-group.teams.appID](./values.yaml#L383) | string | `"APPLICATION_ID"` | The BotKube application ID generated while registering Bot to MS Teams. | -| [communications.default-group.teams.appPassword](./values.yaml#L385) | string | `"APPLICATION_PASSWORD"` | The BotKube application password generated while registering Bot to MS Teams. | -| [communications.default-group.teams.bindings.executors](./values.yaml#L388) | list | `["kubectl-read-only"]` | Executor bindings apply to all MS Teams channels where BotKube has access to. | -| [communications.default-group.teams.bindings.sources](./values.yaml#L391) | list | `["k8s-err-events","k8s-recommendation-events"]` | Source bindings apply to all channels which have notification turned on with `@BotKube notifier start` command. | -| [communications.default-group.teams.messagePath](./values.yaml#L395) | string | `"/bots/teams"` | The path in endpoint URL provided while registering BotKube to MS Teams. | -| [communications.default-group.teams.port](./values.yaml#L397) | int | `3978` | The Service port for bot endpoint on BotKube container. | -| [communications.default-group.discord.enabled](./values.yaml#L402) | bool | `false` | If true, enables Discord bot. | -| [communications.default-group.discord.token](./values.yaml#L404) | string | `"DISCORD_TOKEN"` | BotKube Bot Token. | -| [communications.default-group.discord.botID](./values.yaml#L406) | string | `"DISCORD_BOT_ID"` | BotKube Application Client ID. | -| [communications.default-group.discord.channels](./values.yaml#L410) | object | `{"default":{"bindings":{"executors":["kubectl-read-only"],"sources":["k8s-err-events","k8s-recommendation-events"]},"id":"DISCORD_CHANNEL_ID","notification":{"disabled":false}}}` | Map of configured channels. The property name under `channels` object is an alias for a given configuration. | -| [communications.default-group.discord.channels.default.id](./values.yaml#L414) | string | `"DISCORD_CHANNEL_ID"` | Discord channel ID for receiving BotKube alerts. The BotKube user needs to be added to it. | -| [communications.default-group.discord.channels.default.notification.disabled](./values.yaml#L417) | bool | `false` | If true, the notifications are not sent to the channel. They can be enabled with `@BotKube` command anytime. | -| [communications.default-group.discord.channels.default.bindings.executors](./values.yaml#L420) | list | `["kubectl-read-only"]` | Executors configuration for a given channel. | -| [communications.default-group.discord.channels.default.bindings.sources](./values.yaml#L423) | list | `["k8s-err-events","k8s-recommendation-events"]` | Notification sources configuration for a given channel. | -| [communications.default-group.discord.notification.type](./values.yaml#L428) | string | `"short"` | Configures notification type that are sent. Possible values: `short`, `long`. | -| [communications.default-group.elasticsearch.enabled](./values.yaml#L433) | bool | `false` | If true, enables Elasticsearch. | -| [communications.default-group.elasticsearch.awsSigning.enabled](./values.yaml#L437) | bool | `false` | If true, enables awsSigning using IAM for Elasticsearch hosted on AWS. Make sure AWS environment variables are set. [Ref doc](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html). | -| [communications.default-group.elasticsearch.awsSigning.awsRegion](./values.yaml#L439) | string | `"us-east-1"` | AWS region where Elasticsearch is deployed. | -| [communications.default-group.elasticsearch.awsSigning.roleArn](./values.yaml#L441) | string | `""` | AWS IAM Role arn to assume for credentials, use this only if you don't want to use the EC2 instance role or not running on AWS instance. | -| [communications.default-group.elasticsearch.server](./values.yaml#L443) | string | `"ELASTICSEARCH_ADDRESS"` | The server URL, e.g https://example.com:9243 | -| [communications.default-group.elasticsearch.username](./values.yaml#L445) | string | `"ELASTICSEARCH_USERNAME"` | Basic Auth username. | -| [communications.default-group.elasticsearch.password](./values.yaml#L447) | string | `"ELASTICSEARCH_PASSWORD"` | Basic Auth password. | -| [communications.default-group.elasticsearch.skipTLSVerify](./values.yaml#L450) | bool | `false` | If true, skips the verification of TLS certificate of the Elastic nodes. It's useful for clusters with self-signed certificates. | -| [communications.default-group.elasticsearch.indices](./values.yaml#L454) | object | `{"default":{"bindings":{"sources":["k8s-err-events","k8s-recommendation-events"]},"name":"botkube","replicas":0,"shards":1,"type":"botkube-event"}}` | Map of configured indices. The `indices` property name is an alias for a given configuration. | -| [communications.default-group.elasticsearch.indices.default.name](./values.yaml#L457) | string | `"botkube"` | Configures Elasticsearch index settings. | -| [communications.default-group.elasticsearch.indices.default.bindings.sources](./values.yaml#L463) | list | `["k8s-err-events","k8s-recommendation-events"]` | Notification sources configuration for a given index. | -| [communications.default-group.webhook.enabled](./values.yaml#L470) | bool | `false` | If true, enables Webhook. | -| [communications.default-group.webhook.url](./values.yaml#L472) | string | `"WEBHOOK_URL"` | The Webhook URL, e.g.: https://example.com:80 | -| [communications.default-group.webhook.bindings.sources](./values.yaml#L475) | list | `["k8s-err-events","k8s-recommendation-events"]` | Notification sources configuration for the webhook. | -| [settings.clusterName](./values.yaml#L482) | string | `"not-configured"` | Cluster name to differentiate incoming messages. | -| [settings.configWatcher](./values.yaml#L484) | bool | `true` | If true, restarts the BotKube Pod on config changes. | -| [settings.upgradeNotifier](./values.yaml#L486) | bool | `true` | If true, notifies about new BotKube releases. | -| [settings.log.level](./values.yaml#L490) | string | `"info"` | Sets one of the log levels. Allowed values: `info`, `warn`, `debug`, `error`, `fatal`, `panic`. | -| [settings.log.disableColors](./values.yaml#L492) | bool | `false` | If true, disable ANSI colors in logging. | -| [settings.systemConfigMap](./values.yaml#L495) | object | `{"name":"botkube-system"}` | BotKube's system ConfigMap where internal data is stored. | -| [settings.persistentConfig](./values.yaml#L500) | object | `{"runtime":{"configMap":{"annotations":{},"name":"botkube-runtime-config"},"fileName":"_runtime_state.yaml"},"startup":{"configMap":{"annotations":{},"name":"botkube-startup-config"},"fileName":"__startup_state.yaml"}}` | Persistent config contains ConfigMap where persisted configuration is stored. The persistent configuration is evaluated from both chart upgrade and BotKube commands used in runtime. | -| [ssl.enabled](./values.yaml#L515) | bool | `false` | If true, specify cert path in `config.ssl.cert` property or K8s Secret in `config.ssl.existingSecretName`. | -| [ssl.existingSecretName](./values.yaml#L521) | string | `""` | Using existing SSL Secret. It MUST be in `botkube` Namespace. | -| [ssl.cert](./values.yaml#L524) | string | `""` | SSL Certificate file e.g certs/my-cert.crt. | -| [service](./values.yaml#L527) | object | `{"name":"metrics","port":2112,"targetPort":2112}` | Configures Service settings for ServiceMonitor CR. | -| [ingress](./values.yaml#L534) | 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#L545) | 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#L555) | object | `{}` | Extra annotations to pass to the BotKube Deployment. | -| [extraAnnotations](./values.yaml#L562) | object | `{}` | Extra annotations to pass to the BotKube Pod. | -| [extraLabels](./values.yaml#L564) | object | `{}` | Extra labels to pass to the BotKube Pod. | -| [priorityClassName](./values.yaml#L566) | string | `""` | Priority class name for the BotKube Pod. | -| [nameOverride](./values.yaml#L569) | string | `""` | Fully override "botkube.name" template. | -| [fullnameOverride](./values.yaml#L571) | string | `""` | Fully override "botkube.fullname" template. | -| [resources](./values.yaml#L577) | 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#L589) | 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#L601) | 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#L616) | 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#L634) | object | `{}` | Node labels for BotKube Pod assignment. [Ref doc](https://kubernetes.io/docs/user-guide/node-selection/). | -| [tolerations](./values.yaml#L638) | list | `[]` | Tolerations for BotKube Pod assignment. [Ref doc](https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/). | -| [affinity](./values.yaml#L642) | object | `{}` | Affinity for BotKube Pod assignment. [Ref doc](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity). | -| [rbac](./values.yaml#L646) | 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#L655) | bool | `true` | If true, a ServiceAccount is automatically created. | -| [serviceAccount.name](./values.yaml#L658) | string | `""` | The name of the service account to use. If not set, a name is generated using the fullname template. | -| [serviceAccount.annotations](./values.yaml#L660) | object | `{}` | Extra annotations for the ServiceAccount. | -| [extraObjects](./values.yaml#L663) | list | `[]` | Extra Kubernetes resources to create. Helm templating is allowed as it is evaluated before creating the resources. | -| [analytics.disable](./values.yaml#L691) | 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#L697) | string | `"ghcr.io"` | Test runner image registry. | -| [e2eTest.image.repository](./values.yaml#L699) | string | `"kubeshop/botkube-test"` | Test runner image repository. | -| [e2eTest.image.pullPolicy](./values.yaml#L701) | string | `"IfNotPresent"` | Test runner image pull policy. | -| [e2eTest.image.tag](./values.yaml#L703) | string | `"v9.99.9-dev"` | Test runner image tag. Default tag is `appVersion` from Chart.yaml. | -| [e2eTest.deployment](./values.yaml#L705) | object | `{"waitTimeout":"3m"}` | Configures BotKube Deployment related data. | -| [e2eTest.slack.botName](./values.yaml#L710) | string | `"botkube"` | Name of the BotKube bot to interact with during the e2e tests. | -| [e2eTest.slack.testerName](./values.yaml#L712) | string | `"botkube_tester"` | Name of the BotKube Tester bot that sends messages during the e2e tests. | -| [e2eTest.slack.testerAppToken](./values.yaml#L714) | string | `""` | Slack tester application token that interacts with BotKube bot. | -| [e2eTest.slack.additionalContextMessage](./values.yaml#L716) | 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#L718) | 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#L721) | string | `"botkube"` | Name of the BotKube bot to interact with during the e2e tests. | -| [e2eTest.discord.testerName](./values.yaml#L723) | string | `"botkube_tester"` | Name of the BotKube Tester bot that sends messages during the e2e tests. | -| [e2eTest.discord.guildID](./values.yaml#L725) | string | `""` | Discord Guild ID (discord server ID) used to run e2e tests | -| [e2eTest.discord.testerAppToken](./values.yaml#L727) | string | `""` | Discord tester application token that interacts with BotKube bot. | -| [e2eTest.discord.additionalContextMessage](./values.yaml#L729) | 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#L731) | string | `"1m"` | Message wait timeout. It defines how long we wait to ensure that notification were not sent when disabled. | +| [existingCommunicationsSecretName](./values.yaml#L277) | string | `""` | Configures existing Secret with communication settings. It MUST be in the `botkube` Namespace. To reload BotKube once it changes, add label `botkube.io/config-watch: "true"`. | +| [communications](./values.yaml#L284) | object | See the `values.yaml` file for full object. | Map of communication groups. Communication group contains settings for multiple communication platforms. The property name under `communications` object is an alias for a given configuration group. You can define multiple communication groups with different names. | +| [communications.default-group.slack.enabled](./values.yaml#L289) | bool | `false` | If true, enables Slack bot. | +| [communications.default-group.slack.channels](./values.yaml#L293) | object | `{"default":{"bindings":{"executors":["kubectl-read-only"],"sources":["k8s-err-events","k8s-recommendation-events"]},"name":"SLACK_CHANNEL","notification":{"disabled":false}}}` | Map of configured channels. The property name under `channels` object is an alias for a given configuration. | +| [communications.default-group.slack.channels.default.name](./values.yaml#L296) | string | `"SLACK_CHANNEL"` | Slack channel name without '#' prefix where you have added BotKube and want to receive notifications in. | +| [communications.default-group.slack.channels.default.notification.disabled](./values.yaml#L299) | bool | `false` | If true, the notifications are not sent to the channel. They can be enabled with `@BotKube` command anytime. | +| [communications.default-group.slack.channels.default.bindings.executors](./values.yaml#L302) | list | `["kubectl-read-only"]` | Executors configuration for a given channel. | +| [communications.default-group.slack.channels.default.bindings.sources](./values.yaml#L305) | list | `["k8s-err-events","k8s-recommendation-events"]` | Notification sources configuration for a given channel. | +| [communications.default-group.slack.token](./values.yaml#L309) | string | `""` | Slack token. | +| [communications.default-group.slack.notification.type](./values.yaml#L312) | string | `"short"` | Configures notification type that are sent. Possible values: `short`, `long`. | +| [communications.default-group.socketSlack.enabled](./values.yaml#L317) | bool | `false` | If true, enables Slack bot. | +| [communications.default-group.socketSlack.channels](./values.yaml#L321) | object | `{"default":{"bindings":{"executors":["kubectl-read-only"],"sources":["k8s-err-events","k8s-recommendation-events"]},"name":"SLACK_CHANNEL"}}` | Map of configured channels. The property name under `channels` object is an alias for a given configuration. | +| [communications.default-group.socketSlack.channels.default.name](./values.yaml#L324) | string | `"SLACK_CHANNEL"` | Slack channel name without '#' prefix where you have added BotKube and want to receive notifications in. | +| [communications.default-group.socketSlack.channels.default.bindings.executors](./values.yaml#L327) | list | `["kubectl-read-only"]` | Executors configuration for a given channel. | +| [communications.default-group.socketSlack.channels.default.bindings.sources](./values.yaml#L330) | list | `["k8s-err-events","k8s-recommendation-events"]` | Notification sources configuration for a given channel. | +| [communications.default-group.socketSlack.botToken](./values.yaml#L335) | string | `""` | Slack bot token for your own Slack app. [Ref doc](https://api.slack.com/authentication/token-types). | +| [communications.default-group.socketSlack.appToken](./values.yaml#L338) | string | `""` | Slack app-level token for your own Slack app. [Ref doc](https://api.slack.com/authentication/token-types). | +| [communications.default-group.socketSlack.notification.type](./values.yaml#L341) | string | `"short"` | Configures notification type that are sent. Possible values: `short`, `long`. | +| [communications.default-group.mattermost.enabled](./values.yaml#L345) | bool | `false` | If true, enables Mattermost bot. | +| [communications.default-group.mattermost.botName](./values.yaml#L347) | string | `"BotKube"` | User in Mattermost which belongs the specified Personal Access token. | +| [communications.default-group.mattermost.url](./values.yaml#L349) | string | `"MATTERMOST_SERVER_URL"` | The URL (including http/https schema) where Mattermost is running. e.g https://example.com:9243 | +| [communications.default-group.mattermost.token](./values.yaml#L351) | string | `"MATTERMOST_TOKEN"` | Personal Access token generated by BotKube user. | +| [communications.default-group.mattermost.team](./values.yaml#L353) | string | `"MATTERMOST_TEAM"` | The Mattermost Team name where BotKube is added. | +| [communications.default-group.mattermost.channels](./values.yaml#L357) | object | `{"default":{"bindings":{"executors":["kubectl-read-only"],"sources":["k8s-err-events","k8s-recommendation-events"]},"name":"MATTERMOST_CHANNEL","notification":{"disabled":false}}}` | Map of configured channels. The property name under `channels` object is an alias for a given configuration. | +| [communications.default-group.mattermost.channels.default.name](./values.yaml#L361) | string | `"MATTERMOST_CHANNEL"` | The Mattermost channel name for receiving BotKube alerts. The BotKube user needs to be added to it. | +| [communications.default-group.mattermost.channels.default.notification.disabled](./values.yaml#L364) | bool | `false` | If true, the notifications are not sent to the channel. They can be enabled with `@BotKube` command anytime. | +| [communications.default-group.mattermost.channels.default.bindings.executors](./values.yaml#L367) | list | `["kubectl-read-only"]` | Executors configuration for a given channel. | +| [communications.default-group.mattermost.channels.default.bindings.sources](./values.yaml#L370) | list | `["k8s-err-events","k8s-recommendation-events"]` | Notification sources configuration for a given channel. | +| [communications.default-group.mattermost.notification.type](./values.yaml#L375) | string | `"short"` | Configures notification type that are sent. Possible values: `short`, `long`. | +| [communications.default-group.teams.enabled](./values.yaml#L380) | bool | `false` | If true, enables MS Teams bot. | +| [communications.default-group.teams.botName](./values.yaml#L382) | string | `"BotKube"` | The Bot name set while registering Bot to MS Teams. | +| [communications.default-group.teams.appID](./values.yaml#L384) | string | `"APPLICATION_ID"` | The BotKube application ID generated while registering Bot to MS Teams. | +| [communications.default-group.teams.appPassword](./values.yaml#L386) | string | `"APPLICATION_PASSWORD"` | The BotKube application password generated while registering Bot to MS Teams. | +| [communications.default-group.teams.bindings.executors](./values.yaml#L389) | list | `["kubectl-read-only"]` | Executor bindings apply to all MS Teams channels where BotKube has access to. | +| [communications.default-group.teams.bindings.sources](./values.yaml#L392) | list | `["k8s-err-events","k8s-recommendation-events"]` | Source bindings apply to all channels which have notification turned on with `@BotKube notifier start` command. | +| [communications.default-group.teams.messagePath](./values.yaml#L396) | string | `"/bots/teams"` | The path in endpoint URL provided while registering BotKube to MS Teams. | +| [communications.default-group.teams.port](./values.yaml#L398) | int | `3978` | The Service port for bot endpoint on BotKube container. | +| [communications.default-group.discord.enabled](./values.yaml#L403) | bool | `false` | If true, enables Discord bot. | +| [communications.default-group.discord.token](./values.yaml#L405) | string | `"DISCORD_TOKEN"` | BotKube Bot Token. | +| [communications.default-group.discord.botID](./values.yaml#L407) | string | `"DISCORD_BOT_ID"` | BotKube Application Client ID. | +| [communications.default-group.discord.channels](./values.yaml#L411) | object | `{"default":{"bindings":{"executors":["kubectl-read-only"],"sources":["k8s-err-events","k8s-recommendation-events"]},"id":"DISCORD_CHANNEL_ID","notification":{"disabled":false}}}` | Map of configured channels. The property name under `channels` object is an alias for a given configuration. | +| [communications.default-group.discord.channels.default.id](./values.yaml#L415) | string | `"DISCORD_CHANNEL_ID"` | Discord channel ID for receiving BotKube alerts. The BotKube user needs to be added to it. | +| [communications.default-group.discord.channels.default.notification.disabled](./values.yaml#L418) | bool | `false` | If true, the notifications are not sent to the channel. They can be enabled with `@BotKube` command anytime. | +| [communications.default-group.discord.channels.default.bindings.executors](./values.yaml#L421) | list | `["kubectl-read-only"]` | Executors configuration for a given channel. | +| [communications.default-group.discord.channels.default.bindings.sources](./values.yaml#L424) | list | `["k8s-err-events","k8s-recommendation-events"]` | Notification sources configuration for a given channel. | +| [communications.default-group.discord.notification.type](./values.yaml#L429) | string | `"short"` | Configures notification type that are sent. Possible values: `short`, `long`. | +| [communications.default-group.elasticsearch.enabled](./values.yaml#L434) | bool | `false` | If true, enables Elasticsearch. | +| [communications.default-group.elasticsearch.awsSigning.enabled](./values.yaml#L438) | bool | `false` | If true, enables awsSigning using IAM for Elasticsearch hosted on AWS. Make sure AWS environment variables are set. [Ref doc](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html). | +| [communications.default-group.elasticsearch.awsSigning.awsRegion](./values.yaml#L440) | string | `"us-east-1"` | AWS region where Elasticsearch is deployed. | +| [communications.default-group.elasticsearch.awsSigning.roleArn](./values.yaml#L442) | string | `""` | AWS IAM Role arn to assume for credentials, use this only if you don't want to use the EC2 instance role or not running on AWS instance. | +| [communications.default-group.elasticsearch.server](./values.yaml#L444) | string | `"ELASTICSEARCH_ADDRESS"` | The server URL, e.g https://example.com:9243 | +| [communications.default-group.elasticsearch.username](./values.yaml#L446) | string | `"ELASTICSEARCH_USERNAME"` | Basic Auth username. | +| [communications.default-group.elasticsearch.password](./values.yaml#L448) | string | `"ELASTICSEARCH_PASSWORD"` | Basic Auth password. | +| [communications.default-group.elasticsearch.skipTLSVerify](./values.yaml#L451) | bool | `false` | If true, skips the verification of TLS certificate of the Elastic nodes. It's useful for clusters with self-signed certificates. | +| [communications.default-group.elasticsearch.indices](./values.yaml#L455) | object | `{"default":{"bindings":{"sources":["k8s-err-events","k8s-recommendation-events"]},"name":"botkube","replicas":0,"shards":1,"type":"botkube-event"}}` | Map of configured indices. The `indices` property name is an alias for a given configuration. | +| [communications.default-group.elasticsearch.indices.default.name](./values.yaml#L458) | string | `"botkube"` | Configures Elasticsearch index settings. | +| [communications.default-group.elasticsearch.indices.default.bindings.sources](./values.yaml#L464) | list | `["k8s-err-events","k8s-recommendation-events"]` | Notification sources configuration for a given index. | +| [communications.default-group.webhook.enabled](./values.yaml#L471) | bool | `false` | If true, enables Webhook. | +| [communications.default-group.webhook.url](./values.yaml#L473) | string | `"WEBHOOK_URL"` | The Webhook URL, e.g.: https://example.com:80 | +| [communications.default-group.webhook.bindings.sources](./values.yaml#L476) | list | `["k8s-err-events","k8s-recommendation-events"]` | Notification sources configuration for the webhook. | +| [settings.clusterName](./values.yaml#L483) | string | `"not-configured"` | Cluster name to differentiate incoming messages. | +| [settings.lifecycleServer](./values.yaml#L486) | object | `{"enabled":true,"port":2113}` | Server configuration which exposes functionality related to the app lifecycle. | +| [settings.upgradeNotifier](./values.yaml#L490) | bool | `true` | If true, notifies about new BotKube releases. | +| [settings.log.level](./values.yaml#L494) | string | `"info"` | Sets one of the log levels. Allowed values: `info`, `warn`, `debug`, `error`, `fatal`, `panic`. | +| [settings.log.disableColors](./values.yaml#L496) | bool | `false` | If true, disable ANSI colors in logging. | +| [settings.systemConfigMap](./values.yaml#L499) | object | `{"name":"botkube-system"}` | BotKube's system ConfigMap where internal data is stored. | +| [settings.persistentConfig](./values.yaml#L504) | object | `{"runtime":{"configMap":{"annotations":{},"name":"botkube-runtime-config"},"fileName":"_runtime_state.yaml"},"startup":{"configMap":{"annotations":{},"name":"botkube-startup-config"},"fileName":"_startup_state.yaml"}}` | Persistent config contains ConfigMap where persisted configuration is stored. The persistent configuration is evaluated from both chart upgrade and BotKube commands used in runtime. | +| [ssl.enabled](./values.yaml#L519) | bool | `false` | If true, specify cert path in `config.ssl.cert` property or K8s Secret in `config.ssl.existingSecretName`. | +| [ssl.existingSecretName](./values.yaml#L525) | string | `""` | Using existing SSL Secret. It MUST be in `botkube` Namespace. | +| [ssl.cert](./values.yaml#L528) | string | `""` | SSL Certificate file e.g certs/my-cert.crt. | +| [service](./values.yaml#L531) | object | `{"name":"metrics","port":2112,"targetPort":2112}` | Configures Service settings for ServiceMonitor CR. | +| [ingress](./values.yaml#L538) | 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#L549) | 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#L559) | object | `{}` | Extra annotations to pass to the BotKube Deployment. | +| [extraAnnotations](./values.yaml#L566) | object | `{}` | Extra annotations to pass to the BotKube Pod. | +| [extraLabels](./values.yaml#L568) | object | `{}` | Extra labels to pass to the BotKube Pod. | +| [priorityClassName](./values.yaml#L570) | string | `""` | Priority class name for the BotKube Pod. | +| [nameOverride](./values.yaml#L573) | string | `""` | Fully override "botkube.name" template. | +| [fullnameOverride](./values.yaml#L575) | string | `""` | Fully override "botkube.fullname" template. | +| [resources](./values.yaml#L581) | 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#L593) | 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#L605) | 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#L620) | 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#L638) | object | `{}` | Node labels for BotKube Pod assignment. [Ref doc](https://kubernetes.io/docs/user-guide/node-selection/). | +| [tolerations](./values.yaml#L642) | list | `[]` | Tolerations for BotKube Pod assignment. [Ref doc](https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/). | +| [affinity](./values.yaml#L646) | object | `{}` | Affinity for BotKube Pod assignment. [Ref doc](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity). | +| [rbac](./values.yaml#L650) | 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#L659) | bool | `true` | If true, a ServiceAccount is automatically created. | +| [serviceAccount.name](./values.yaml#L662) | string | `""` | The name of the service account to use. If not set, a name is generated using the fullname template. | +| [serviceAccount.annotations](./values.yaml#L664) | object | `{}` | Extra annotations for the ServiceAccount. | +| [extraObjects](./values.yaml#L667) | list | `[]` | Extra Kubernetes resources to create. Helm templating is allowed as it is evaluated before creating the resources. | +| [analytics.disable](./values.yaml#L695) | bool | `false` | If true, sending anonymous analytics is disabled. To learn what date we collect, see [Privacy Policy](https://botkube.io/privacy#privacy-policy). | +| [configWatcher.enabled](./values.yaml#L700) | bool | `true` | If true, restarts the BotKube Pod on config changes. | +| [configWatcher.tmpDir](./values.yaml#L702) | string | `"/tmp/watched-cfg/"` | Directory, where watched configuration resources are stored. | +| [configWatcher.initialSyncTimeout](./values.yaml#L705) | int | `0` | Timeout for the initial Config Watcher sync. If set to 0, waiting for Config Watcher sync will be skipped. In a result, configuration changes may not reload BotKube app during the first few seconds after BotKube startup. | +| [configWatcher.image.registry](./values.yaml#L708) | string | `"ghcr.io"` | Config watcher image registry. | +| [configWatcher.image.repository](./values.yaml#L710) | string | `"kubeshop/k8s-sidecar"` | Config watcher image repository. | +| [configWatcher.image.tag](./values.yaml#L712) | string | `"ignore-initial-events"` | Config watcher image tag. | +| [configWatcher.image.pullPolicy](./values.yaml#L714) | string | `"IfNotPresent"` | Config watcher image pull policy. | +| [e2eTest.image.registry](./values.yaml#L720) | string | `"ghcr.io"` | Test runner image registry. | +| [e2eTest.image.repository](./values.yaml#L722) | string | `"kubeshop/botkube-test"` | Test runner image repository. | +| [e2eTest.image.pullPolicy](./values.yaml#L724) | string | `"IfNotPresent"` | Test runner image pull policy. | +| [e2eTest.image.tag](./values.yaml#L726) | string | `"v9.99.9-dev"` | Test runner image tag. Default tag is `appVersion` from Chart.yaml. | +| [e2eTest.deployment](./values.yaml#L728) | object | `{"waitTimeout":"3m"}` | Configures BotKube Deployment related data. | +| [e2eTest.slack.botName](./values.yaml#L733) | string | `"botkube"` | Name of the BotKube bot to interact with during the e2e tests. | +| [e2eTest.slack.testerName](./values.yaml#L735) | string | `"botkube_tester"` | Name of the BotKube Tester bot that sends messages during the e2e tests. | +| [e2eTest.slack.testerAppToken](./values.yaml#L737) | string | `""` | Slack tester application token that interacts with BotKube bot. | +| [e2eTest.slack.additionalContextMessage](./values.yaml#L739) | 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#L741) | 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#L744) | string | `"botkube"` | Name of the BotKube bot to interact with during the e2e tests. | +| [e2eTest.discord.testerName](./values.yaml#L746) | string | `"botkube_tester"` | Name of the BotKube Tester bot that sends messages during the e2e tests. | +| [e2eTest.discord.guildID](./values.yaml#L748) | string | `""` | Discord Guild ID (discord server ID) used to run e2e tests | +| [e2eTest.discord.testerAppToken](./values.yaml#L750) | string | `""` | Discord tester application token that interacts with BotKube bot. | +| [e2eTest.discord.additionalContextMessage](./values.yaml#L752) | 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#L754) | 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/communicationsecret.yaml b/helm/botkube/templates/communicationsecret.yaml index 0a8028a82..d34c604cf 100644 --- a/helm/botkube/templates/communicationsecret.yaml +++ b/helm/botkube/templates/communicationsecret.yaml @@ -8,6 +8,7 @@ metadata: helm.sh/chart: {{ include "botkube.chart" . }} app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/managed-by: {{ .Release.Service }} + botkube.io/config-watch: "true" stringData: comm_config.yaml: | # Communication settings diff --git a/helm/botkube/templates/deployment.yaml b/helm/botkube/templates/deployment.yaml index 170c62f0e..4c0196ac6 100644 --- a/helm/botkube/templates/deployment.yaml +++ b/helm/botkube/templates/deployment.yaml @@ -15,6 +15,8 @@ metadata: {{- end }} spec: replicas: {{ .Values.replicaCount }} + strategy: + type: Recreate # RollingUpdate doesn't work with SocketSlack integration as it requires a single connection to Slack API. selector: matchLabels: component: controller @@ -70,6 +72,8 @@ spec: {{ end }} - name: cache mountPath: "/.kube/cache" + - name: cfg-watcher-tmp + mountPath: {{ .Values.configWatcher.tmpDir }} env: - name: BOTKUBE_CONFIG_PATHS value: "/config/global_config.yaml,/config/comm_config.yaml,/config/{{ .Values.settings.persistentConfig.runtime.fileName}},/startup-config/{{ .Values.settings.persistentConfig.startup.fileName}}" @@ -85,6 +89,10 @@ spec: value: "{{.Release.Namespace}}" - name: BOTKUBE_SETTINGS_PERSISTENT__CONFIG_STARTUP_CONFIG__MAP_NAMESPACE value: "{{.Release.Namespace}}" + - name: BOTKUBE_SETTINGS_LIFECYCLE__SERVER_DEPLOYMENT_NAMESPACE + value: "{{.Release.Namespace}}" + - name: BOTKUBE_SETTINGS_LIFECYCLE__SERVER_DEPLOYMENT_NAME + value: "{{ include "botkube.fullname" . }}" {{- with .Values.extraEnv }} {{ toYaml . | nindent 12 }} {{- end }} @@ -92,7 +100,38 @@ spec: resources: {{ toYaml .Values.resources | indent 12 }} {{- end }} + {{- if .Values.configWatcher.enabled }} + - name: cfg-watcher + image: "{{ .Values.configWatcher.image.registry }}/{{ .Values.configWatcher.image.repository }}:{{ .Values.configWatcher.image.tag }}" + imagePullPolicy: "{{ .Values.configWatcher.image.pullPolicy }}" + env: + - name: FOLDER + value: {{ .Values.configWatcher.tmpDir }} + - name: RESOURCE + value: "both" + - name: LOG_LEVEL + value: "DEBUG" + - name: NAMESPACE + value: "{{.Release.Namespace}}" + - name: LABEL + value: "botkube.io/config-watch" + - name: LABEL_VALUE + value: "true" + - name: REQ_URL + value: "http://{{ include "botkube.fullname" . }}:{{.Values.settings.lifecycleServer.port}}/reload" + - name: REQ_IGNORE_INITIAL_EVENT + value: "true" + - name: REQ_METHOD + value: "POST" + - name: IGNORE_ALREADY_PROCESSED + value: "true" + volumeMounts: + - name: cfg-watcher-tmp + mountPath: {{ .Values.configWatcher.tmpDir }} + {{- end }} volumes: + - name: cfg-watcher-tmp + emptyDir: {} - name: config-volume projected: sources: diff --git a/helm/botkube/templates/global-config.yaml b/helm/botkube/templates/global-config.yaml index 9bec78dd6..9eac11d87 100644 --- a/helm/botkube/templates/global-config.yaml +++ b/helm/botkube/templates/global-config.yaml @@ -7,6 +7,7 @@ metadata: helm.sh/chart: {{ include "botkube.chart" . }} app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/managed-by: {{ .Release.Service }} + botkube.io/config-watch: "true" data: global_config.yaml: | executors: @@ -21,5 +22,8 @@ data: filters: {{- .Values.filters | toYaml | nindent 6 }} + configWatcher: + {{- .Values.configWatcher | toYaml | nindent 6 }} + analytics: disable: {{ .Values.analytics.disable }} diff --git a/helm/botkube/templates/persistent-config.yaml b/helm/botkube/templates/persistent-config.yaml index 3d883fdd4..33e1b8cc6 100644 --- a/helm/botkube/templates/persistent-config.yaml +++ b/helm/botkube/templates/persistent-config.yaml @@ -12,6 +12,7 @@ metadata: helm.sh/chart: {{ include "botkube.chart" . }} app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/managed-by: {{ .Release.Service }} + botkube.io/config-watch: "true" data: {{- $prevRuntimeCfgMap := lookup "v1" "ConfigMap" .Release.Namespace $runtimeStateCfgMap | default dict }} {{- $prevRuntimeFile := index ( $prevRuntimeCfgMap.data | default dict ) .Values.settings.persistentConfig.runtime.fileName | default "" | fromYaml -}} @@ -66,14 +67,13 @@ metadata: helm.sh/chart: {{ include "botkube.chart" . }} app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/managed-by: {{ .Release.Service }} + botkube.io/config-watch: "false" # Explicitly don't watch this ConfigMap data: {{- $prevStartupCfgMap := lookup "v1" "ConfigMap" .Release.Namespace $startupStateCfgMap | default dict }} {{- $prevStartupFile := index ( $prevStartupCfgMap.data | default dict ) .Values.settings.persistentConfig.startup.fileName | default "" | fromYaml -}} {{- $mergedStartupCommunications := mustMergeOverwrite (mustDeepCopy (default (dict) $prevStartupFile.communications )) (mustDeepCopy .Values.communications) }} {{- $mergedStartupFilters := mustMergeOverwrite (mustDeepCopy (default (dict) $prevStartupFile.filters )) (mustDeepCopy (default (dict) .Values.filters)) }} - # This file has a special prefix to: - # - load it as the last config file during BotKube startup, - # - ignore it by Config Watcher. + # This file has a special prefix to load it as the last config file during BotKube startup. {{ .Values.settings.persistentConfig.startup.fileName }}: | communications: {{- range $commGroupName,$commGroup := $mergedStartupCommunications }} diff --git a/helm/botkube/templates/service.yaml b/helm/botkube/templates/service.yaml index 4a70b64ce..cb81eb717 100644 --- a/helm/botkube/templates/service.yaml +++ b/helm/botkube/templates/service.yaml @@ -1,4 +1,4 @@ -{{- if or .Values.serviceMonitor.enabled (include "botkube.communication.team.enabled" $) }} +{{- if or .Values.serviceMonitor.enabled (include "botkube.communication.team.enabled" $) (.Values.settings.lifecycleServer.enabled ) }} apiVersion: v1 kind: Service metadata: @@ -12,6 +12,11 @@ metadata: spec: type: ClusterIP ports: + {{- if .Values.settings.lifecycleServer.enabled }} + - name: "lifecycle" + port: {{ .Values.settings.lifecycleServer.port }} + targetPort: {{ .Values.settings.lifecycleServer.port }} + {{- end }} {{- if .Values.serviceMonitor.enabled }} - name: {{ .Values.service.name }} port: {{ .Values.service.port }} diff --git a/helm/botkube/templates/botkube-cm.yaml b/helm/botkube/templates/system-config.yaml similarity index 80% rename from helm/botkube/templates/botkube-cm.yaml rename to helm/botkube/templates/system-config.yaml index 23cd3e24d..f72c7ad42 100644 --- a/helm/botkube/templates/botkube-cm.yaml +++ b/helm/botkube/templates/system-config.yaml @@ -7,3 +7,4 @@ metadata: helm.sh/chart: {{ include "botkube.chart" . }} app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/managed-by: {{ .Release.Service }} + botkube.io/config-watch: "false" # Explicitly don't watch this ConfigMap diff --git a/helm/botkube/templates/systemroles.yaml b/helm/botkube/templates/systemroles.yaml index 165e76a49..46961a308 100644 --- a/helm/botkube/templates/systemroles.yaml +++ b/helm/botkube/templates/systemroles.yaml @@ -12,6 +12,14 @@ rules: - apiGroups: [""] resources: ["configmaps"] verbs: ["update", "get", "create"] + - apiGroups: [""] + resources: ["configmaps", "secrets"] + verbs: ["get", "watch", "list"] +{{- if .Values.settings.lifecycleServer.enabled }} + - apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["patch"] +{{ end }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding diff --git a/helm/botkube/values.yaml b/helm/botkube/values.yaml index 40cef9cb8..08bc3f87c 100644 --- a/helm/botkube/values.yaml +++ b/helm/botkube/values.yaml @@ -267,6 +267,7 @@ executors: # -- Configures existing Secret with communication settings. It MUST be in the `botkube` Namespace. +# To reload BotKube once it changes, add label `botkube.io/config-watch: "true"`. ## Secret format: ## stringData: ## comm_config.yaml: | @@ -480,8 +481,11 @@ communications: settings: # -- Cluster name to differentiate incoming messages. clusterName: not-configured - # -- If true, restarts the BotKube Pod on config changes. - configWatcher: true + + # -- Server configuration which exposes functionality related to the app lifecycle. + lifecycleServer: + enabled: true + port: 2113 # -- If true, notifies about new BotKube releases. upgradeNotifier: true ## BotKube logging settings. @@ -502,7 +506,7 @@ settings: configMap: name: botkube-startup-config annotations: {} - fileName: "__startup_state.yaml" + fileName: "_startup_state.yaml" runtime: configMap: name: botkube-runtime-config @@ -690,6 +694,25 @@ analytics: # see [Privacy Policy](https://botkube.io/privacy#privacy-policy). disable: false +## Parameters for the config watcher container. +configWatcher: + # -- If true, restarts the BotKube Pod on config changes. + enabled: true + # -- Directory, where watched configuration resources are stored. + tmpDir: "/tmp/watched-cfg/" + # -- Timeout for the initial Config Watcher sync. + # If set to 0, waiting for Config Watcher sync will be skipped. In a result, configuration changes may not reload BotKube app during the first few seconds after BotKube startup. + initialSyncTimeout: 0 + image: + # -- Config watcher image registry. + registry: ghcr.io + # -- Config watcher image repository. + repository: kubeshop/k8s-sidecar # kiwigrid/k8s-sidecar:1.19.5 - see https://github.com/kubeshop/k8s-sidecar/pull/1 + # -- Config watcher image tag. + tag: ignore-initial-events + # -- Config watcher image pull policy. + pullPolicy: IfNotPresent + ## Parameters for the test container with E2E tests. e2eTest: image: diff --git a/internal/lifecycle/server.go b/internal/lifecycle/server.go new file mode 100644 index 000000000..6ae0e29ba --- /dev/null +++ b/internal/lifecycle/server.go @@ -0,0 +1,69 @@ +package lifecycle + +import ( + "fmt" + "net/http" + "time" + + "github.com/gorilla/mux" + "github.com/sirupsen/logrus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + + "github.com/kubeshop/botkube/pkg/config" + "github.com/kubeshop/botkube/pkg/httpsrv" +) + +const ( + k8sDeploymentRestartPatchFmt = `{"spec":{"template":{"metadata":{"annotations":{"kubectl.kubernetes.io/restartedAt":"%s"}}}}}` + reloadMsgFmt = ":arrows_counterclockwise: Configuration reload requested for cluster '%s'. Hold on a sec..." +) + +// SendMessageFn defines a function which sends a given message. +type SendMessageFn func(msg string) error + +// NewServer creates a new httpsrv.Server that exposes lifecycle methods as HTTP endpoints. +func NewServer(log logrus.FieldLogger, k8sCli kubernetes.Interface, cfg config.LifecycleServer, clusterName string, sendMsgFn SendMessageFn) *httpsrv.Server { + addr := fmt.Sprintf(":%d", cfg.Port) + router := mux.NewRouter() + reloadHandler := newReloadHandler(log, k8sCli, cfg.Deployment, clusterName, sendMsgFn) + router.HandleFunc("/reload", reloadHandler) + return httpsrv.New(log, addr, router) +} + +func newReloadHandler(log logrus.FieldLogger, k8sCli kubernetes.Interface, deploy config.K8sResourceRef, clusterName string, sendMsgFn SendMessageFn) http.HandlerFunc { + return func(writer http.ResponseWriter, request *http.Request) { + log.Info("Reload requested. Sending last message before exit...") + err := sendMsgFn(fmt.Sprintf(reloadMsgFmt, clusterName)) + if err != nil { + errMsg := fmt.Sprintf("while sending last message: %s", err.Error()) + log.Errorf(errMsg) + + // continue anyway, this is a non-blocking error + } + + log.Infof(`Reloading te the deployment "%s/%s"...`, deploy.Namespace, deploy.Name) + // This is what `kubectl rollout restart` does. + restartData := fmt.Sprintf(k8sDeploymentRestartPatchFmt, time.Now().String()) + ctx := request.Context() + _, err = k8sCli.AppsV1().Deployments(deploy.Namespace).Patch( + ctx, + deploy.Name, + types.StrategicMergePatchType, + []byte(restartData), + metav1.PatchOptions{FieldManager: "kubectl-rollout"}, + ) + if err != nil { + errMsg := fmt.Sprintf("while restarting the Deployment: %s", err.Error()) + log.Error(errMsg) + http.Error(writer, errMsg, http.StatusInternalServerError) + } + + writer.WriteHeader(http.StatusOK) + _, err = writer.Write([]byte(fmt.Sprintf(`Deployment "%s/%s" restarted successfully.`, deploy.Namespace, deploy.Name))) + if err != nil { + log.Errorf("while writing success response: %s", err.Error()) + } + } +} diff --git a/internal/lifecycle/server_test.go b/internal/lifecycle/server_test.go new file mode 100644 index 000000000..beef08d13 --- /dev/null +++ b/internal/lifecycle/server_test.go @@ -0,0 +1,67 @@ +package lifecycle + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + + logtest "github.com/sirupsen/logrus/hooks/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" + + "github.com/kubeshop/botkube/pkg/config" +) + +func TestNewReloadHandler_HappyPath(t *testing.T) { + // given + clusterName := "foo" + + expectedMsg := fmt.Sprintf(":arrows_counterclockwise: Configuration reload requested for cluster '%s'. Hold on a sec...", clusterName) + expectedResponse := `Deployment "namespace/name" restarted successfully.` + + expectedStatusCode := http.StatusOK + deployCfg := config.K8sResourceRef{ + Name: "name", + Namespace: "namespace", + } + inputDeploy := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: deployCfg.Name, + Namespace: deployCfg.Namespace, + }, + } + sendMsgFn := SendMessageFn(func(msg string) error { + assert.Equal(t, expectedMsg, msg) + return nil + }) + logger, _ := logtest.NewNullLogger() + k8sCli := fake.NewSimpleClientset(inputDeploy) + + req := httptest.NewRequest(http.MethodPost, "/reload", nil) + writer := httptest.NewRecorder() + handler := newReloadHandler(logger, k8sCli, deployCfg, clusterName, sendMsgFn) + + // when + handler(writer, req) + + res := writer.Result() + defer res.Body.Close() + data, err := io.ReadAll(res.Body) + require.NoError(t, err) + + // then + assert.Equal(t, expectedStatusCode, res.StatusCode) + assert.Equal(t, expectedResponse, string(data)) + + actualDeploy, err := k8sCli.AppsV1().Deployments(deployCfg.Namespace).Get(context.Background(), deployCfg.Name, metav1.GetOptions{}) + require.NoError(t, err) + + _, exists := actualDeploy.Spec.Template.Annotations["kubectl.kubernetes.io/restartedAt"] + assert.True(t, exists) +} diff --git a/pkg/bot/bot.go b/pkg/bot/bot.go index 3292017bf..87beb26f5 100644 --- a/pkg/bot/bot.go +++ b/pkg/bot/bot.go @@ -4,15 +4,15 @@ import ( "context" "github.com/kubeshop/botkube/pkg/config" - "github.com/kubeshop/botkube/pkg/controller" "github.com/kubeshop/botkube/pkg/execute" + "github.com/kubeshop/botkube/pkg/notifier" ) // Bot connects to communication channels and reads/sends messages. It is a two-way integration. type Bot interface { Start(ctx context.Context) error BotName() string - controller.Notifier + notifier.Notifier } // ExecutorFactory facilitates creation of execute.Executor instances. diff --git a/pkg/bot/teams.go b/pkg/bot/teams.go index 3d569e354..c8c8650d6 100644 --- a/pkg/bot/teams.go +++ b/pkg/bot/teams.go @@ -71,7 +71,8 @@ type Teams struct { conversations map[string]conversation notifyMutex sync.Mutex botMentionRegex *regexp.Regexp - mdFormatter interactive.MDFormatter + longFormatter interactive.MDFormatter + shortFormatter interactive.MDFormatter botName string AppID string @@ -102,7 +103,9 @@ func NewTeams(log logrus.FieldLogger, commGroupName string, cfg config.Teams, cl if msgPath == "" { msgPath = "/" } - mdFormatter := interactive.NewMDFormatter(mdLineFormatter, interactive.DefaultMDHeaderFormatter) + longFormatter := interactive.NewMDFormatter(longLineFormatter, interactive.DefaultMDHeaderFormatter) + shortFormatter := interactive.NewMDFormatter(shortLineFormatter, interactive.DefaultMDHeaderFormatter) + return &Teams{ log: log, executorFactory: executorFactory, @@ -118,7 +121,8 @@ func NewTeams(log logrus.FieldLogger, commGroupName string, cfg config.Teams, cl Port: port, conversations: make(map[string]conversation), botMentionRegex: botMentionRegex, - mdFormatter: mdFormatter, + longFormatter: longFormatter, + shortFormatter: shortFormatter, }, nil } @@ -299,9 +303,9 @@ func (b *Teams) convertInteractiveMessage(in interactive.Message) string { if in.HasSections() { // MS Teams doesn't respect multiple new lines, so it needs to be rendered // with `
` tags instead Β―\_(ツ)_/Β― - return interactive.MessageToMarkdown(b.mdFormatter, in) + return interactive.MessageToMarkdown(b.longFormatter, in) } - return interactive.MessageToMarkdown(b.mdFormatter, in) + return interactive.MessageToMarkdown(b.shortFormatter, in) } func (b *Teams) putRequest(u string, data []byte) (err error) { @@ -536,14 +540,20 @@ func teamsBotMentionRegex(botName string) (*regexp.Regexp, error) { return botMentionRegex, nil } -// MSTeamsLineFmt represents new line formatting for MS Teams. +// longLineFormatter represents new line formatting for MS Teams where message has multiple sections. // Unfortunately, it's different from all others integrations. -func mdLineFormatter(msg string) string { +func longLineFormatter(msg string) string { // e.g. `:rocket:` is not supported by MS Teams, so we need to replace it with actual emoji msg = replaceEmojiTagsWithActualOne(msg) return fmt.Sprintf("%s
", msg) } +func shortLineFormatter(msg string) string { + // e.g. `:rocket:` is not supported by MS Teams, so we need to replace it with actual emoji + msg = replaceEmojiTagsWithActualOne(msg) + return fmt.Sprintf("%s\n", msg) +} + // replaceEmojiTagsWithActualOne replaces the emoji tag with actual emoji. func replaceEmojiTagsWithActualOne(content string) string { return mdEmojiTag.ReplaceAllStringFunc(content, func(s string) string { @@ -553,5 +563,9 @@ func replaceEmojiTagsWithActualOne(content string) string { // emojiMapping holds mapping between emoji tags and actual ones. var emojiMapping = map[string]string{ - ":rocket:": "πŸš€", + ":rocket:": "πŸš€", + ":white_check_mark:": "βœ…", + ":arrows_counterclockwise:": "πŸ”„", + ":crossed_fingers:": "🀞", + ":exclamation:": "❗", } diff --git a/pkg/config/config.go b/pkg/config/config.go index 4c857695e..806a9ac77 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -25,12 +25,11 @@ var defaultConfiguration []byte var configPathsFlag []string const ( - configEnvVariablePrefix = "BOTKUBE_" - configDelimiter = "." - camelCaseDelimiter = "__" - nestedFieldDelimiter = "_" - specialConfigFileNamePrefix = "_" - specialIgnoredConfigFileNamePrefix = "__" + configEnvVariablePrefix = "BOTKUBE_" + configDelimiter = "." + camelCaseDelimiter = "__" + nestedFieldDelimiter = "_" + specialConfigFileNamePrefix = "_" ) const ( @@ -130,8 +129,9 @@ type Config struct { Communications map[string]Communications `yaml:"communications" validate:"required,min=1,dive"` Filters Filters `yaml:"filters"` - Analytics Analytics `yaml:"analytics"` - Settings Settings `yaml:"settings"` + Analytics Analytics `yaml:"analytics"` + Settings Settings `yaml:"settings"` + ConfigWatcher CfgWatcher `yaml:"configWatcher"` } // ChannelBindingsByName contains configuration bindings per channel. @@ -497,14 +497,21 @@ type Commands struct { Resources []string `yaml:"resources"` } +// CfgWatcher describes configuration for watching the configuration. +type CfgWatcher struct { + Enabled bool `yaml:"enabled"` + InitialSyncTimeout time.Duration `yaml:"initialSyncTimeout"` + TmpDir string `yaml:"tmpDir"` +} + // Settings contains BotKube's related configuration. type Settings struct { ClusterName string `yaml:"clusterName"` - ConfigWatcher bool `yaml:"configWatcher"` UpgradeNotifier bool `yaml:"upgradeNotifier"` - SystemConfigMap K8sConfigMapRef `yaml:"systemConfigMap"` + SystemConfigMap K8sResourceRef `yaml:"systemConfigMap"` PersistentConfig PersistentConfig `yaml:"persistentConfig"` MetricsPort string `yaml:"metricsPort"` + LifecycleServer LifecycleServer `yaml:"lifecycleServer"` Log struct { Level string `yaml:"level"` DisableColors bool `yaml:"disableColors"` @@ -513,6 +520,13 @@ type Settings struct { Kubeconfig string `yaml:"kubeconfig"` } +// LifecycleServer contains configuration for the server with app lifecycle methods. +type LifecycleServer struct { + Enabled bool `yaml:"enabled"` + Port int `yaml:"port"` // String for consistency + Deployment K8sResourceRef `yaml:"deployment"` +} + // PersistentConfig contains configuration for persistent storage. type PersistentConfig struct { Startup PartialPersistentConfig `yaml:"startup"` @@ -521,12 +535,12 @@ type PersistentConfig struct { // PartialPersistentConfig contains configuration for persistent storage of a given type. type PartialPersistentConfig struct { - FileName string `yaml:"fileName"` - ConfigMap K8sConfigMapRef `yaml:"configMap"` + FileName string `yaml:"fileName"` + ConfigMap K8sResourceRef `yaml:"configMap"` } -// K8sConfigMapRef holds the configuration for a ConfigMap. -type K8sConfigMapRef struct { +// K8sResourceRef holds the configuration for a Kubernetes resource. +type K8sResourceRef struct { Name string `yaml:"name,omitempty"` Namespace string `yaml:"namespace,omitempty"` } @@ -540,7 +554,6 @@ type PathsGetter func() []string // LoadWithDefaultsDetails holds the LoadWithDefaults function details. type LoadWithDefaultsDetails struct { - CfgFilesToWatch []string ValidateWarnings error } @@ -587,7 +600,6 @@ func LoadWithDefaults(getCfgPaths PathsGetter) (*Config, LoadWithDefaultsDetails } return &cfg, LoadWithDefaultsDetails{ - CfgFilesToWatch: getCfgFilesToWatch(configPaths), ValidateWarnings: result.Warnings.ErrorOrNil(), }, nil } @@ -643,22 +655,6 @@ func sortCfgFiles(paths []string) []string { return append(ordinaryCfgFiles, specialCfgFiles...) } -// getCfgFilesToWatch excludes the files that has specialIgnoredConfigFileNamePrefix from the paths. -func getCfgFilesToWatch(paths []string) []string { - var filesToWatch []string - for _, path := range paths { - _, filename := filepath.Split(path) - - if strings.HasPrefix(filename, specialIgnoredConfigFileNamePrefix) { - continue - } - - filesToWatch = append(filesToWatch, path) - } - - return filesToWatch -} - // IdentifiableMap provides an option to construct an indexable map for identifiable items. type IdentifiableMap[T Identifiable] map[string]T diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 8ba7c7e5f..10117e5de 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -270,42 +270,18 @@ func TestIsNamespaceAllowed(t *testing.T) { } } -func TestGetCfgFilesToWatch(t *testing.T) { - tests := map[string]struct { - input []string - expected []string - }{ - "No special files": { - input: []string{"config.yaml", ".bar.yaml", "/_foo/bar.yaml", "/__bar/baz.yaml"}, - expected: []string{"config.yaml", ".bar.yaml", "/_foo/bar.yaml", "/__bar/baz.yaml"}, - }, - "Special files should be ignored": { - input: []string{"_test.yaml", "config.yaml", "__foo.yaml", ".bar.yaml", "/bar/__baz.yaml", "/baz/_qux.yaml"}, - expected: []string{"_test.yaml", "config.yaml", ".bar.yaml", "/baz/_qux.yaml"}, - }, - } - - for name, test := range tests { - name, test := name, test - t.Run(name, func(t *testing.T) { - actual := config.GetCfgFilesToWatch(test.input) - assert.Equal(t, test.expected, actual) - }) - } -} - func TestSortCfgFiles(t *testing.T) { tests := map[string]struct { input []string expected []string }{ "No special files": { - input: []string{"config.yaml", ".bar.yaml", "/_foo/bar.yaml", "/__bar/baz.yaml"}, - expected: []string{"config.yaml", ".bar.yaml", "/_foo/bar.yaml", "/__bar/baz.yaml"}, + input: []string{"config.yaml", ".bar.yaml", "/_foo/bar.yaml", "/_bar/baz.yaml"}, + expected: []string{"config.yaml", ".bar.yaml", "/_foo/bar.yaml", "/_bar/baz.yaml"}, }, "Special files": { - input: []string{"_test.yaml", "config.yaml", "_foo.yaml", "__foo.yaml", ".bar.yaml", "/bar/__baz.yaml", "/bar/_baz.yaml"}, - expected: []string{"config.yaml", ".bar.yaml", "_test.yaml", "_foo.yaml", "__foo.yaml", "/bar/__baz.yaml", "/bar/_baz.yaml"}, + input: []string{"_test.yaml", "config.yaml", "_foo.yaml", ".bar.yaml", "/bar/_baz.yaml"}, + expected: []string{"config.yaml", ".bar.yaml", "_test.yaml", "_foo.yaml", "/bar/_baz.yaml"}, }, } diff --git a/pkg/config/export_test.go b/pkg/config/export_test.go index 6204e1a8c..738e32814 100644 --- a/pkg/config/export_test.go +++ b/pkg/config/export_test.go @@ -4,10 +4,6 @@ func NormalizeConfigEnvName(name string) string { return normalizeConfigEnvName(name) } -func GetCfgFilesToWatch(paths []string) []string { - return getCfgFilesToWatch(paths) -} - func SortCfgFiles(paths []string) []string { return sortCfgFiles(paths) } diff --git a/pkg/config/manager_test.go b/pkg/config/manager_test.go index 4b3f97f73..71449faaa 100644 --- a/pkg/config/manager_test.go +++ b/pkg/config/manager_test.go @@ -19,7 +19,7 @@ func TestPersistenceManager_PersistSourceBindings(t *testing.T) { // given commGroupName := "default-group" cfg := config.PartialPersistentConfig{ - ConfigMap: config.K8sConfigMapRef{ + ConfigMap: config.K8sResourceRef{ Name: "foo", Namespace: "ns", }, @@ -275,7 +275,7 @@ func TestPersistenceManager_PersistNotificationsEnabled(t *testing.T) { // given commGroupName := "default-group" cfg := config.PartialPersistentConfig{ - ConfigMap: config.K8sConfigMapRef{ + ConfigMap: config.K8sResourceRef{ Name: "foo", Namespace: "ns", }, @@ -428,7 +428,7 @@ func TestPersistenceManager_PersistNotificationsEnabled(t *testing.T) { func TestPersistenceManager_PersistFilterEnabled(t *testing.T) { // given cfg := config.PartialPersistentConfig{ - ConfigMap: config.K8sConfigMapRef{ + ConfigMap: config.K8sResourceRef{ Name: "foo", Namespace: "ns", }, diff --git a/pkg/config/testdata/TestLoadConfigSuccess/config.golden.yaml b/pkg/config/testdata/TestLoadConfigSuccess/config.golden.yaml index 20461dfa7..709ee4cd0 100644 --- a/pkg/config/testdata/TestLoadConfigSuccess/config.golden.yaml +++ b/pkg/config/testdata/TestLoadConfigSuccess/config.golden.yaml @@ -389,7 +389,6 @@ analytics: disable: true settings: clusterName: cluster-name-from-env - configWatcher: true upgradeNotifier: true systemConfigMap: name: botkube-system @@ -404,8 +403,16 @@ settings: configMap: name: runtime-config metricsPort: "1313" + lifecycleServer: + enabled: false + port: 0 + deployment: {} log: level: error disableColors: false informersResyncPeriod: 30m0s kubeconfig: kubeconfig-from-env +configWatcher: + enabled: false + initialSyncTimeout: 0s + tmpDir: "" diff --git a/pkg/config/watcher_sync.go b/pkg/config/watcher_sync.go new file mode 100644 index 000000000..3af58045c --- /dev/null +++ b/pkg/config/watcher_sync.go @@ -0,0 +1,44 @@ +package config + +import ( + "context" + "os" + "time" + + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/util/wait" +) + +const ( + watcherPollInterval = 200 * time.Millisecond +) + +// WaitForWatcherSync delays startup until ConfigWatcher synchronizes at least one configuration file +func WaitForWatcherSync(ctx context.Context, log logrus.FieldLogger, cfg CfgWatcher) error { + if cfg.InitialSyncTimeout.Milliseconds() == 0 { + log.Info("Skipping waiting for Config Watcher sync...") + return nil + } + + log.Infof("Waiting for synchronized files in directory %q with timeout %s...", cfg.TmpDir, cfg.InitialSyncTimeout) + err := wait.PollWithContext(ctx, watcherPollInterval, cfg.InitialSyncTimeout, func(ctx context.Context) (done bool, err error) { + files, err := os.ReadDir(cfg.TmpDir) + if err != nil { + return false, err + } + + for _, file := range files { + if file.IsDir() { + // skip subdirectories + continue + } + + log.Infof("File %q detected. Finishing polling...", file.Name()) + return true, nil + } + + return false, nil + }) + + return err +} diff --git a/pkg/controller/config_watcher.go b/pkg/controller/config_watcher.go deleted file mode 100644 index 2d06cf0c1..000000000 --- a/pkg/controller/config_watcher.go +++ /dev/null @@ -1,94 +0,0 @@ -package controller - -import ( - "context" - "fmt" - - "github.com/fsnotify/fsnotify" - "github.com/sirupsen/logrus" - "golang.org/x/sync/errgroup" - - "github.com/kubeshop/botkube/pkg/multierror" -) - -// ConfigWatcher watches for the config file changes and exits the app. -// TODO: It keeps the previous behavior for now, but it should hot-reload the configuration files without needing a restart. -type ConfigWatcher struct { - log logrus.FieldLogger - configPaths []string - clusterName string - notifiers []Notifier -} - -// NewConfigWatcher returns new ConfigWatcher instance. -func NewConfigWatcher(log logrus.FieldLogger, configPaths []string, clusterName string, notifiers []Notifier) *ConfigWatcher { - return &ConfigWatcher{ - log: log, - configPaths: configPaths, - clusterName: clusterName, - notifiers: notifiers, - } -} - -// Do starts watching the configuration file -func (w *ConfigWatcher) Do(ctx context.Context, cancelFunc context.CancelFunc) (err error) { - watcher, err := fsnotify.NewWatcher() - if err != nil { - return fmt.Errorf("while creating file watcher: %w", err) - } - defer func() { - deferredErr := watcher.Close() - if deferredErr != nil { - err = multierror.Append(err, deferredErr) - } - }() - - ctx, cancelFn := context.WithCancel(ctx) - defer cancelFn() - - log := w.log.WithField("configPaths", w.configPaths) - - errGroup, _ := errgroup.WithContext(ctx) - errGroup.Go(func() error { - for { - select { - case <-ctx.Done(): - log.Info("Shutdown requested. Finishing...") - return nil - case ev, ok := <-watcher.Events: - if !ok { - return fmt.Errorf("unexpected file watch end") - } - - currentLogg := log.WithField("event", ev.String()) - - currentLogg.Info("Config updated. Sending last message before exit...") - err := sendMessageToNotifiers(ctx, w.notifiers, fmt.Sprintf(configUpdateMsg, w.clusterName)) - if err != nil { - wrappedErr := fmt.Errorf("while sending message to notifiers: %w", err) - //do not exit yet, cancel the context first - cancelFunc() - return wrappedErr - } - - currentLogg.Infof("Cancelling the context...") - cancelFunc() - return nil - case err, ok := <-watcher.Errors: - if !ok { - return fmt.Errorf("unexpected file watch end") - } - return fmt.Errorf("while reading events for config files: %w", err) - } - } - }) - - log.Infof("Registering watcher on config files") - for _, path := range w.configPaths { - err = watcher.Add(path) - if err != nil { - return fmt.Errorf("while registering watch on config file %q: %w", path, err) - } - } - return errGroup.Wait() -} diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 4eece8886..6623a3d43 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -18,15 +18,15 @@ import ( "github.com/kubeshop/botkube/pkg/events" "github.com/kubeshop/botkube/pkg/filterengine" "github.com/kubeshop/botkube/pkg/multierror" + "github.com/kubeshop/botkube/pkg/notifier" "github.com/kubeshop/botkube/pkg/recommendation" "github.com/kubeshop/botkube/pkg/sources" "github.com/kubeshop/botkube/pkg/utils" ) const ( - controllerStartMsg = "...and now my watch begins for cluster '%s'! :crossed_swords:" - controllerStopMsg = "My watch has ended for cluster '%s'. Hope will be back online soon! :crossed_fingers:" - configUpdateMsg = "Looks like the configuration is updated for cluster '%s'. I shall halt my watch till I read it." + controllerStartMsg = "My watch begins for cluster '%s'! :crossed_swords:" + controllerStopMsg = "My watch has ended for cluster '%s'. See you soon! :crossed_fingers:" finalMessageTimeout = 20 * time.Second ) @@ -57,7 +57,7 @@ type Controller struct { reporter AnalyticsReporter startTime time.Time conf *config.Config - notifiers []Notifier + notifiers []notifier.Notifier recommFactory RecommendationFactory filterEngine filterengine.FilterEngine informersResyncPeriod time.Duration @@ -72,7 +72,7 @@ type Controller struct { // New create a new Controller instance. func New(log logrus.FieldLogger, conf *config.Config, - notifiers []Notifier, + notifiers []notifier.Notifier, recommFactory RecommendationFactory, filterEngine filterengine.FilterEngine, dynamicCli dynamic.Interface, @@ -97,6 +97,7 @@ func New(log logrus.FieldLogger, // Start creates new informer controllers to watch k8s resources func (c *Controller) Start(ctx context.Context) error { + c.log.Info("Starting controller...") c.dynamicKubeInformerFactory = dynamicinformer.NewDynamicSharedInformerFactory(c.dynamicCli, c.informersResyncPeriod) err := c.sourcesRouter.RegisterInformers([]config.EventType{ @@ -197,8 +198,8 @@ func (c *Controller) Start(ctx context.Context) error { } }) - c.log.Info("Starting controller") - err = sendMessageToNotifiers(ctx, c.notifiers, fmt.Sprintf(controllerStartMsg, c.conf.Settings.ClusterName)) + c.log.Info("Sending welcome message...") + err = notifier.SendPlaintextMessage(ctx, c.notifiers, fmt.Sprintf(controllerStartMsg, c.conf.Settings.ClusterName)) if err != nil { return fmt.Errorf("while sending first message: %w", err) } @@ -206,14 +207,14 @@ func (c *Controller) Start(ctx context.Context) error { c.startTime = time.Now() stopCh := ctx.Done() - c.dynamicKubeInformerFactory.Start(stopCh) + <-stopCh c.log.Info("Shutdown requested. Sending final message...") finalMsgCtx, cancelFn := context.WithTimeout(context.Background(), finalMessageTimeout) defer cancelFn() - err = sendMessageToNotifiers(finalMsgCtx, c.notifiers, fmt.Sprintf(controllerStopMsg, c.conf.Settings.ClusterName)) + err = notifier.SendPlaintextMessage(finalMsgCtx, c.notifiers, fmt.Sprintf(controllerStopMsg, c.conf.Settings.ClusterName)) if err != nil { return fmt.Errorf("while sending final message: %w", err) } @@ -286,7 +287,7 @@ func (c *Controller) sendEvent(ctx context.Context, obj interface{}, resource st // Send event over notifiers anonymousEvent := analytics.AnonymizedEventDetailsFrom(event) for _, n := range c.notifiers { - go func(n Notifier) { + go func(n notifier.Notifier) { defer analytics.ReportPanicIfOccurs(c.log, c.reporter) err := n.SendEvent(ctx, event, sources) diff --git a/pkg/controller/upgrade.go b/pkg/controller/upgrade.go index 018bf0017..4a7c09c2a 100644 --- a/pkg/controller/upgrade.go +++ b/pkg/controller/upgrade.go @@ -9,6 +9,7 @@ import ( "github.com/google/go-github/v44/github" "github.com/sirupsen/logrus" + "github.com/kubeshop/botkube/pkg/notifier" "github.com/kubeshop/botkube/pkg/version" ) @@ -27,12 +28,12 @@ type GitHubRepoClient interface { // UpgradeChecker checks for new BotKube releases. type UpgradeChecker struct { log logrus.FieldLogger - notifiers []Notifier + notifiers []notifier.Notifier ghRepoCli GitHubRepoClient } // NewUpgradeChecker creates a new instance of the Upgrade Checker. -func NewUpgradeChecker(log logrus.FieldLogger, notifiers []Notifier, ghCli GitHubRepoClient) *UpgradeChecker { +func NewUpgradeChecker(log logrus.FieldLogger, notifiers []notifier.Notifier, ghCli GitHubRepoClient) *UpgradeChecker { return &UpgradeChecker{log: log, notifiers: notifiers, ghRepoCli: ghCli} } @@ -90,7 +91,7 @@ func (c *UpgradeChecker) notifyAboutUpgradeIfShould(ctx context.Context) (bool, return false, nil } - err = sendMessageToNotifiers(ctx, c.notifiers, fmt.Sprintf(upgradeMsgFmt, *release.TagName)) + err = notifier.SendPlaintextMessage(ctx, c.notifiers, fmt.Sprintf(upgradeMsgFmt, *release.TagName)) if err != nil { return false, fmt.Errorf("while sending message about new release: %w", err) } diff --git a/pkg/execute/edit.go b/pkg/execute/edit.go index 382e1dea8..af5fc3c47 100644 --- a/pkg/execute/edit.go +++ b/pkg/execute/edit.go @@ -17,8 +17,9 @@ import ( ) const ( - editedSourcesMsgFmt = ":white_check_mark: %s adjusted the BotKube notifications settings to %s messages. Expect BotKube restart soon..." - unknownSourcesMsgFmt = ":exclamation: The %s %s not found in configuration. To learn how to add custom source, visit https://botkube.io/docs/configuration/source." + editedSourcesMsgFmt = ":white_check_mark: %s adjusted the BotKube notifications settings to %s messages. Expect BotKube reload in a few seconds..." + editedSourcesMsgWithoutReloadFmt = ":white_check_mark: %s adjusted the BotKube notifications settings to %s messages.\nAs the Config Watcher is disabled, you need to restart BotKube manually to apply the changes." + unknownSourcesMsgFmt = ":exclamation: The %s %s not found in configuration. To learn how to add custom source, visit https://botkube.io/docs/configuration/source." ) // EditResource defines the name of editable resource @@ -150,13 +151,22 @@ func (e *EditExecutor) editSourceBindingHandler(cmdArgs []string, commGroupName if userID == "" { userID = "Anonymous" } + return interactive.Message{ Base: interactive.Base{ - Description: fmt.Sprintf(editedSourcesMsgFmt, userID, sourceList), + Description: e.getEditedSourceBindingsMsg(userID, sourceList), }, }, nil } +func (e *EditExecutor) getEditedSourceBindingsMsg(userID, sourceList string) string { + if !e.cfg.ConfigWatcher.Enabled { + return fmt.Sprintf(editedSourcesMsgWithoutReloadFmt, userID, sourceList) + } + + return fmt.Sprintf(editedSourcesMsgFmt, userID, sourceList) +} + func (e *EditExecutor) generateUnknownMessage(unknown []string) interactive.Message { list := english.OxfordWordSeries(e.quoteEachItem(unknown), "and") word := english.PluralWord(len(unknown), "source was", "sources were") diff --git a/pkg/execute/edit_test.go b/pkg/execute/edit_test.go index 519de4dc4..8778ef76d 100644 --- a/pkg/execute/edit_test.go +++ b/pkg/execute/edit_test.go @@ -43,11 +43,16 @@ func TestSourceBindingsHappyPath(t *testing.T) { DisplayName: "BAZ", }, }, + ConfigWatcher: config.CfgWatcher{ + Enabled: true, + }, } + cfgWithCfgWatcherDisabled := config.Config{Sources: cfg.Sources} tests := []struct { name string command string + config config.Config message string sourceBindings []string @@ -55,52 +60,67 @@ func TestSourceBindingsHappyPath(t *testing.T) { { name: "Should resolve quoted list which is separated by comma", command: `edit SourceBindings "bar,xyz"`, + config: cfg, - message: ":white_check_mark: Joe adjusted the BotKube notifications settings to BAR and XYZ messages. Expect BotKube restart soon...", + message: ":white_check_mark: Joe adjusted the BotKube notifications settings to BAR and XYZ messages. Expect BotKube reload in a few seconds...", sourceBindings: []string{"bar", "xyz"}, }, { name: "Should resolve quoted and code items separated by comma", command: "edit sourcebindings β€œ`bar`,xyz ”", + config: cfg, - message: ":white_check_mark: Joe adjusted the BotKube notifications settings to BAR and XYZ messages. Expect BotKube restart soon...", + message: ":white_check_mark: Joe adjusted the BotKube notifications settings to BAR and XYZ messages. Expect BotKube reload in a few seconds...", sourceBindings: []string{"bar", "xyz"}, }, { name: "Should resolve list which is separated by comma and ends with whitespace", command: `edit sourceBindings bar,xyz `, + config: cfg, - message: ":white_check_mark: Joe adjusted the BotKube notifications settings to BAR and XYZ messages. Expect BotKube restart soon...", + message: ":white_check_mark: Joe adjusted the BotKube notifications settings to BAR and XYZ messages. Expect BotKube reload in a few seconds...", sourceBindings: []string{"bar", "xyz"}, }, { name: "Should resolve list which is separated by comma but has a lot of whitespaces", command: `edit sourcebindings bar, xyz, `, + config: cfg, - message: ":white_check_mark: Joe adjusted the BotKube notifications settings to BAR and XYZ messages. Expect BotKube restart soon...", + message: ":white_check_mark: Joe adjusted the BotKube notifications settings to BAR and XYZ messages. Expect BotKube reload in a few seconds...", sourceBindings: []string{"bar", "xyz"}, }, { name: "Should resolve list which is separated by comma, has a lot of whitespaces and some items are quoted", command: `edit SourceBindings bar xyz, "baz"`, + config: cfg, - message: ":white_check_mark: Joe adjusted the BotKube notifications settings to BAR, XYZ, and BAZ messages. Expect BotKube restart soon...", + message: ":white_check_mark: Joe adjusted the BotKube notifications settings to BAR, XYZ, and BAZ messages. Expect BotKube reload in a few seconds...", sourceBindings: []string{"bar", "xyz", "baz"}, }, { name: "Should resolve list with unicode quotes", command: `edit SourceBindings β€œfoo,bar”`, + config: cfg, - message: ":white_check_mark: Joe adjusted the BotKube notifications settings to FOO and BAR messages. Expect BotKube restart soon...", + message: ":white_check_mark: Joe adjusted the BotKube notifications settings to FOO and BAR messages. Expect BotKube reload in a few seconds...", sourceBindings: []string{"foo", "bar"}, }, { name: "Should resolve list which has mixed formatting for different items, all at once", command: `edit SourceBindings foo baz "bar,xyz" "fiz"`, + config: cfg, - message: ":white_check_mark: Joe adjusted the BotKube notifications settings to FOO, BAZ, BAR, XYZ, and FIZ messages. Expect BotKube restart soon...", + message: ":white_check_mark: Joe adjusted the BotKube notifications settings to FOO, BAZ, BAR, XYZ, and FIZ messages. Expect BotKube reload in a few seconds...", sourceBindings: []string{"foo", "baz", "bar", "xyz", "fiz"}, }, + { + name: "Should mention manual app restart", + command: `edit SourceBindings "bar,xyz"`, + config: cfgWithCfgWatcherDisabled, + + message: ":white_check_mark: Joe adjusted the BotKube notifications settings to BAR and XYZ messages.\nAs the Config Watcher is disabled, you need to restart BotKube manually to apply the changes.", + sourceBindings: []string{"bar", "xyz"}, + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { @@ -109,7 +129,7 @@ func TestSourceBindingsHappyPath(t *testing.T) { fakeStorage := &fakeBindingsStorage{} args := strings.Fields(strings.TrimSpace(tc.command)) - executor := NewEditExecutor(log, &fakeAnalyticsReporter{}, fakeStorage, cfg) + executor := NewEditExecutor(log, &fakeAnalyticsReporter{}, fakeStorage, tc.config) expMessage := interactive.Message{ Base: interactive.Base{ diff --git a/pkg/execute/notifier_test.go b/pkg/execute/notifier_test.go index 9d57fb8fa..b6e760486 100644 --- a/pkg/execute/notifier_test.go +++ b/pkg/execute/notifier_test.go @@ -94,7 +94,6 @@ func TestNotifierExecutor_Do_Success(t *testing.T) { disable: false settings: clusterName: foo - configWatcher: false upgradeNotifier: false systemConfigMap: {} persistentConfig: @@ -105,11 +104,19 @@ func TestNotifierExecutor_Do_Success(t *testing.T) { fileName: "" configMap: {} metricsPort: "" + lifecycleServer: + enabled: false + port: 0 + deployment: {} log: level: "" disableColors: false informersResyncPeriod: 0s kubeconfig: "" + configWatcher: + enabled: false + initialSyncTimeout: 0s + tmpDir: "" `), ExpectedStatusAfter: `Notifications from cluster 'cluster-name' are disabled here.`, }, diff --git a/pkg/controller/notifier.go b/pkg/notifier/notifier.go similarity index 85% rename from pkg/controller/notifier.go rename to pkg/notifier/notifier.go index 4268067c2..65aeba568 100644 --- a/pkg/controller/notifier.go +++ b/pkg/notifier/notifier.go @@ -1,4 +1,4 @@ -package controller +package notifier import ( "context" @@ -17,7 +17,7 @@ type Notifier interface { // SendMessage 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. + // TODO: Consider option per channel to turn on/off "announcements" (BotKube start/stop/upgrade, notify/config change). SendMessage(context.Context, interactive.Message) error // IntegrationName returns a name of a given communication platform. @@ -27,7 +27,8 @@ type Notifier interface { Type() config.IntegrationType } -func sendMessageToNotifiers(ctx context.Context, notifiers []Notifier, msg string) error { +// SendPlaintextMessage sends a plaintext message to specified providers. +func SendPlaintextMessage(ctx context.Context, notifiers []Notifier, msg string) error { if msg == "" { return fmt.Errorf("message cannot be empty") } diff --git a/pkg/sink/types.go b/pkg/sink/types.go index 124127e1d..ce694c275 100644 --- a/pkg/sink/types.go +++ b/pkg/sink/types.go @@ -2,12 +2,12 @@ package sink import ( "github.com/kubeshop/botkube/pkg/config" - "github.com/kubeshop/botkube/pkg/controller" + "github.com/kubeshop/botkube/pkg/notifier" ) // Sink sends messages to communication channels. It is a one-way integration. type Sink interface { - controller.Notifier + notifier.Notifier } // AnalyticsReporter defines a reporter that collects analytics data for sinks. diff --git a/test/e2e/bots_test.go b/test/e2e/bots_test.go index 2897c407d..dea335882 100644 --- a/test/e2e/bots_test.go +++ b/test/e2e/bots_test.go @@ -176,7 +176,7 @@ func runBotTest(t *testing.T, interactive.Help(config.CommPlatformIntegration(botDriver.Type()), appCfg.ClusterName, botDriver.BotName()), ) require.NoError(t, err) - err = botDriver.WaitForMessagePostedRecentlyEqual(botDriver.BotUserID(), botDriver.Channel().ID(), fmt.Sprintf("...and now my watch begins for cluster '%s'! :crossed_swords:", appCfg.ClusterName)) + err = botDriver.WaitForMessagePostedRecentlyEqual(botDriver.BotUserID(), botDriver.Channel().ID(), fmt.Sprintf("My watch begins for cluster '%s'! :crossed_swords:", appCfg.ClusterName)) require.NoError(t, err) t.Log("Running actual test cases")