From feebe049edaadeb97f0ced66fd4f2c1515934b99 Mon Sep 17 00:00:00 2001 From: Mateusz Szostok Date: Tue, 28 Mar 2023 12:52:57 +0200 Subject: [PATCH 1/2] Remove built-in kubectl, filters, defaultNamespace, fix list bugs --- build/Dockerfile | 7 - cmd/botkube/main.go | 63 +-- helm/botkube/README.md | 245 ++++---- helm/botkube/e2e-test-values.yaml | 226 +++++--- helm/botkube/values.yaml | 26 - internal/executor/helm/executor.go | 4 +- internal/executor/kubectl/executor.go | 4 +- internal/plugin/kubeconfig.go | 5 +- internal/source/dispatcher.go | 8 +- pkg/bot/interactive/help.go | 80 +-- pkg/bot/interactive/markdown_test.go | 2 +- pkg/bot/interactive/plaintext_test.go | 2 +- pkg/bot/interactive/plugin_help.go | 42 +- ...m_headers_and_default_new_lines.golden.txt | 13 +- ...m_new_lines_and_default_headers.golden.txt | 2 +- ...stInteractiveMessageToPlaintext.golden.txt | 13 +- pkg/config/config.go | 153 +---- pkg/config/config_test.go | 66 --- .../_aaa-special-file.yaml | 5 - .../TestLoadConfigSuccess/actions.yaml | 2 +- .../TestLoadConfigSuccess/config-all.yaml | 17 +- .../TestLoadConfigSuccess/config.golden.yaml | 376 +------------ .../TestLoadConfigSuccess/executors.yaml | 28 +- .../TestLoadConfigWithPlugins/config-all.yaml | 2 +- .../missing-alias-command.yaml | 2 +- .../executors-include-warning.yaml | 25 - pkg/config/validator.go | 7 - pkg/execute/alias_test.go | 12 +- pkg/execute/default_runner.go | 65 --- pkg/execute/exec.go | 12 +- pkg/execute/exec_test.go | 38 +- pkg/execute/executor.go | 54 +- pkg/execute/factory.go | 27 - pkg/execute/kubectl.go | 294 ---------- pkg/execute/kubectl/checker.go | 45 -- pkg/execute/kubectl/checker_test.go | 140 ----- pkg/execute/kubectl/commander.go | 115 ---- pkg/execute/kubectl/commander_test.go | 226 -------- pkg/execute/kubectl/helpers_test.go | 67 --- pkg/execute/kubectl/merger.go | 149 ----- pkg/execute/kubectl/merger_test.go | 197 ------- pkg/execute/kubectl/resource-normalizer.go | 84 --- pkg/execute/kubectl_cmd_builder.go | 519 ----------------- pkg/execute/kubectl_cmd_builder_msg.go | 228 -------- pkg/execute/kubectl_cmd_builder_test.go | 530 ------------------ pkg/execute/kubectl_test.go | 495 ---------------- pkg/execute/mapping.go | 7 +- pkg/execute/plugin_discovery.go | 17 + pkg/execute/source.go | 8 - pkg/execute/source_test.go | 25 +- test/e2e/bots_test.go | 72 ++- test/e2e/slack_driver_test.go | 8 +- 52 files changed, 506 insertions(+), 4353 deletions(-) delete mode 100644 pkg/config/testdata/TestLoadedConfigValidationWarnings/executors-include-warning.yaml delete mode 100644 pkg/execute/default_runner.go delete mode 100644 pkg/execute/kubectl.go delete mode 100644 pkg/execute/kubectl/checker.go delete mode 100644 pkg/execute/kubectl/checker_test.go delete mode 100644 pkg/execute/kubectl/commander.go delete mode 100644 pkg/execute/kubectl/commander_test.go delete mode 100644 pkg/execute/kubectl/helpers_test.go delete mode 100644 pkg/execute/kubectl/merger.go delete mode 100644 pkg/execute/kubectl/merger_test.go delete mode 100644 pkg/execute/kubectl/resource-normalizer.go delete mode 100644 pkg/execute/kubectl_cmd_builder.go delete mode 100644 pkg/execute/kubectl_cmd_builder_msg.go delete mode 100644 pkg/execute/kubectl_cmd_builder_test.go delete mode 100644 pkg/execute/kubectl_test.go create mode 100644 pkg/execute/plugin_discovery.go diff --git a/build/Dockerfile b/build/Dockerfile index def64a088..8db8b15e9 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -8,13 +8,6 @@ LABEL org.opencontainers.image.source="git@github.com:kubeshop/botkube.git" \ org.opencontainers.image.licenses="MIT" COPY botkube /usr/local/bin/botkube -# Download the latest kubectl in the appropriate architecture. Currently handles aarch64 (arm64) and x86_64 (amd64). -RUN MACH=$(uname -m); if [[ ${MACH} == "aarch64" ]]; then ARCH=arm64; \ - elif [[ ${MACH} == "x86_64" ]]; then ARCH=amd64; \ - elif [[ ${MACH} == "armv7l" ]]; then ARCH=arm; \ - else echo "Unsupported arch: ${MACH}"; ARCH=${MACH}; fi; \ - wget -O /usr/local/bin/kubectl "https://dl.k8s.io/release/$(wget -qO - https://dl.k8s.io/release/stable.txt)/bin/linux/${ARCH}/kubectl" && \ - chmod +x /usr/local/bin/kubectl # Create Non Privileged user RUN addgroup --gid 1001 botkube && \ diff --git a/cmd/botkube/main.go b/cmd/botkube/main.go index a9e7086cd..1619d1c93 100644 --- a/cmd/botkube/main.go +++ b/cmd/botkube/main.go @@ -2,7 +2,6 @@ package main import ( "context" - "encoding/json" "errors" "fmt" "log" @@ -45,7 +44,6 @@ import ( "github.com/kubeshop/botkube/pkg/config" "github.com/kubeshop/botkube/pkg/controller" "github.com/kubeshop/botkube/pkg/execute" - "github.com/kubeshop/botkube/pkg/execute/kubectl" "github.com/kubeshop/botkube/pkg/httpsrv" "github.com/kubeshop/botkube/pkg/notifier" "github.com/kubeshop/botkube/pkg/sink" @@ -172,43 +170,19 @@ func run(ctx context.Context) error { return metricsSrv.Serve(ctx) }) - // Kubectl config merger - kcMerger := kubectl.NewMerger(conf.Executors) - - // Load resource variants name if needed - var resourceNameNormalizerFunc kubectl.ResourceVariantsFunc - if kcMerger.IsAtLeastOneEnabled() { - resourceNameNormalizer, err := kubectl.NewResourceNormalizer( - logger.WithField(componentLogFieldKey, "Resource Name Normalizer"), - discoveryCli, - ) - if err != nil { - return reportFatalError("while creating resource name normalizer", err) - } - resourceNameNormalizerFunc = resourceNameNormalizer.Normalize - } - cmdGuard := command.NewCommandGuard(logger.WithField(componentLogFieldKey, "Command Guard"), discoveryCli) - - runner := &execute.OSCommand{} - k8sVersion, err := findK8sVersion(runner) + botkubeVersion, err := findVersions(k8sCli) if err != nil { - return reportFatalError("while fetching kubernetes version", err) + return reportFatalError("while fetching versions", err) } - botkubeVersion := findBotkubeVersion(k8sVersion) - // Create executor factory cfgManager := config.NewManager(remoteCfgEnabled, logger.WithField(componentLogFieldKey, "Config manager"), conf.Settings.PersistentConfig, cfgVersion, k8sCli, gqlClient, deployClient) executorFactory, err := execute.NewExecutorFactory( execute.DefaultExecutorFactoryParams{ Log: logger.WithField(componentLogFieldKey, "Executor"), - CmdRunner: runner, Cfg: *conf, - KcChecker: kubectl.NewChecker(resourceNameNormalizerFunc), - Merger: kcMerger, CfgManager: cfgManager, AnalyticsReporter: reporter, - NamespaceLister: k8sCli.CoreV1().Namespaces(), CommandGuard: cmdGuard, PluginManager: pluginManager, BotKubeVersion: botkubeVersion, @@ -556,41 +530,16 @@ func sendHelp(ctx context.Context, s *storage.Help, clusterName string, executor return s.MarkHelpAsSent(ctx, sent) } -func findK8sVersion(runner *execute.OSCommand) (string, error) { - type kubectlVersionOutput struct { - Server struct { - GitVersion string `json:"gitVersion"` - } `json:"serverVersion"` - } - - args := []string{"-c", fmt.Sprintf("%s version --output=json", execute.KubectlBinary)} - stdout, stderr, err := runner.RunSeparateOutput("sh", args) +func findVersions(cli *kubernetes.Clientset) (string, error) { + k8sVer, err := cli.ServerVersion() if err != nil { - return "", fmt.Errorf("unable to execute kubectl version: %w [%q]", err, stderr) - } - - var out kubectlVersionOutput - err = json.Unmarshal([]byte(stdout), &out) - if err != nil { - return "", err - } - if out.Server.GitVersion == "" { - return "", fmt.Errorf("unable to unmarshal server git version from %q", stdout) + return "", fmt.Errorf("while getting server version: %w", err) } - ver := out.Server.GitVersion - if stderr != "" { - ver += "\n" + stderr - } - - return ver, nil -} - -func findBotkubeVersion(k8sVersion string) (versions string) { botkubeVersion := version.Short() if len(botkubeVersion) == 0 { botkubeVersion = "Unknown" } - return fmt.Sprintf("K8s Server Version: %s\nBotkube version: %s", k8sVersion, botkubeVersion) + return fmt.Sprintf("K8s Server Version: %s\nBotkube version: %s", k8sVer.String(), botkubeVersion), nil } diff --git a/helm/botkube/README.md b/helm/botkube/README.md index d2af323fa..f2c8c1f96 100644 --- a/helm/botkube/README.md +++ b/helm/botkube/README.md @@ -47,31 +47,31 @@ Controller for the Botkube Slack app which helps you monitor your Kubernetes clu | [sources](./values.yaml#L113) | object | See the `values.yaml` file for full object. | Map of sources. Source contains configuration for Kubernetes events and sending recommendations. The property name under `sources` object is an alias for a given configuration. You can define multiple sources configuration with different names. Key name is used as a binding reference. | | [sources.k8s-recommendation-events.botkube/kubernetes](./values.yaml#L118) | object | See the `values.yaml` file for full object. | Describes Kubernetes source configuration. | | [sources.k8s-recommendation-events.botkube/kubernetes.context.rbac](./values.yaml#L122) | object | `{"group":{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"}}` | RBAC configuration for this plugin. | -| [sources.k8s-err-events.botkube/kubernetes.context.rbac](./values.yaml#L122) | object | `{"group":{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"}}` | RBAC configuration for this plugin. | +| [sources.k8s-all-events.botkube/kubernetes.context.rbac](./values.yaml#L122) | object | `{"group":{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"}}` | RBAC configuration for this plugin. | | [sources.k8s-create-events.botkube/kubernetes.context.rbac](./values.yaml#L122) | object | `{"group":{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"}}` | RBAC configuration for this plugin. | -| [sources.k8s-err-with-logs-events.botkube/kubernetes.context.rbac](./values.yaml#L122) | object | `{"group":{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"}}` | RBAC configuration for this plugin. | -| [executors.k8s-default-tools.botkube/helm.context.rbac](./values.yaml#L122) | object | `{"group":{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"}}` | RBAC configuration for this plugin. | | [executors.k8s-default-tools.botkube/kubectl.context.rbac](./values.yaml#L122) | object | `{"group":{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"}}` | RBAC configuration for this plugin. | -| [sources.k8s-all-events.botkube/kubernetes.context.rbac](./values.yaml#L122) | object | `{"group":{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"}}` | RBAC configuration for this plugin. | -| [sources.k8s-err-events.botkube/kubernetes.context.rbac.group.type](./values.yaml#L125) | string | `"Static"` | Static impersonation for a given username and groups. | -| [executors.k8s-default-tools.botkube/kubectl.context.rbac.group.type](./values.yaml#L125) | string | `"Static"` | Static impersonation for a given username and groups. | -| [sources.k8s-err-with-logs-events.botkube/kubernetes.context.rbac.group.type](./values.yaml#L125) | string | `"Static"` | Static impersonation for a given username and groups. | +| [executors.k8s-default-tools.botkube/helm.context.rbac](./values.yaml#L122) | object | `{"group":{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"}}` | RBAC configuration for this plugin. | +| [sources.k8s-err-with-logs-events.botkube/kubernetes.context.rbac](./values.yaml#L122) | object | `{"group":{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"}}` | RBAC configuration for this plugin. | +| [sources.k8s-err-events.botkube/kubernetes.context.rbac](./values.yaml#L122) | object | `{"group":{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"}}` | RBAC configuration for this plugin. | +| [executors.k8s-default-tools.botkube/helm.context.rbac.group.type](./values.yaml#L125) | string | `"Static"` | Static impersonation for a given username and groups. | | [sources.k8s-create-events.botkube/kubernetes.context.rbac.group.type](./values.yaml#L125) | string | `"Static"` | Static impersonation for a given username and groups. | +| [sources.k8s-err-events.botkube/kubernetes.context.rbac.group.type](./values.yaml#L125) | string | `"Static"` | Static impersonation for a given username and groups. | | [sources.k8s-recommendation-events.botkube/kubernetes.context.rbac.group.type](./values.yaml#L125) | string | `"Static"` | Static impersonation for a given username and groups. | +| [sources.k8s-err-with-logs-events.botkube/kubernetes.context.rbac.group.type](./values.yaml#L125) | string | `"Static"` | Static impersonation for a given username and groups. | +| [executors.k8s-default-tools.botkube/kubectl.context.rbac.group.type](./values.yaml#L125) | string | `"Static"` | Static impersonation for a given username and groups. | | [sources.k8s-all-events.botkube/kubernetes.context.rbac.group.type](./values.yaml#L125) | string | `"Static"` | Static impersonation for a given username and groups. | -| [executors.k8s-default-tools.botkube/helm.context.rbac.group.type](./values.yaml#L125) | string | `"Static"` | Static impersonation for a given username and groups. | -| [executors.k8s-default-tools.botkube/helm.context.rbac.group.prefix](./values.yaml#L127) | string | `""` | Prefix that will be applied to .static.value[*]. | +| [sources.k8s-create-events.botkube/kubernetes.context.rbac.group.prefix](./values.yaml#L127) | string | `""` | Prefix that will be applied to .static.value[*]. | | [sources.k8s-err-with-logs-events.botkube/kubernetes.context.rbac.group.prefix](./values.yaml#L127) | string | `""` | Prefix that will be applied to .static.value[*]. | -| [executors.k8s-default-tools.botkube/kubectl.context.rbac.group.prefix](./values.yaml#L127) | string | `""` | Prefix that will be applied to .static.value[*]. | -| [sources.k8s-recommendation-events.botkube/kubernetes.context.rbac.group.prefix](./values.yaml#L127) | string | `""` | Prefix that will be applied to .static.value[*]. | +| [executors.k8s-default-tools.botkube/helm.context.rbac.group.prefix](./values.yaml#L127) | string | `""` | Prefix that will be applied to .static.value[*]. | | [sources.k8s-err-events.botkube/kubernetes.context.rbac.group.prefix](./values.yaml#L127) | string | `""` | Prefix that will be applied to .static.value[*]. | +| [sources.k8s-recommendation-events.botkube/kubernetes.context.rbac.group.prefix](./values.yaml#L127) | string | `""` | Prefix that will be applied to .static.value[*]. | | [sources.k8s-all-events.botkube/kubernetes.context.rbac.group.prefix](./values.yaml#L127) | string | `""` | Prefix that will be applied to .static.value[*]. | -| [sources.k8s-create-events.botkube/kubernetes.context.rbac.group.prefix](./values.yaml#L127) | string | `""` | Prefix that will be applied to .static.value[*]. | +| [executors.k8s-default-tools.botkube/kubectl.context.rbac.group.prefix](./values.yaml#L127) | string | `""` | Prefix that will be applied to .static.value[*]. | +| [sources.k8s-create-events.botkube/kubernetes.context.rbac.group.static.values](./values.yaml#L130) | list | `["botkube-plugins-default"]` | Name of group.rbac.authorization.k8s.io the plugin will be bound to. | | [executors.k8s-default-tools.botkube/kubectl.context.rbac.group.static.values](./values.yaml#L130) | list | `["botkube-plugins-default"]` | Name of group.rbac.authorization.k8s.io the plugin will be bound to. | +| [sources.k8s-recommendation-events.botkube/kubernetes.context.rbac.group.static.values](./values.yaml#L130) | list | `["botkube-plugins-default"]` | Name of group.rbac.authorization.k8s.io the plugin will be bound to. | | [sources.k8s-err-with-logs-events.botkube/kubernetes.context.rbac.group.static.values](./values.yaml#L130) | list | `["botkube-plugins-default"]` | Name of group.rbac.authorization.k8s.io the plugin will be bound to. | -| [sources.k8s-create-events.botkube/kubernetes.context.rbac.group.static.values](./values.yaml#L130) | list | `["botkube-plugins-default"]` | Name of group.rbac.authorization.k8s.io the plugin will be bound to. | | [executors.k8s-default-tools.botkube/helm.context.rbac.group.static.values](./values.yaml#L130) | list | `["botkube-plugins-default"]` | Name of group.rbac.authorization.k8s.io the plugin will be bound to. | -| [sources.k8s-recommendation-events.botkube/kubernetes.context.rbac.group.static.values](./values.yaml#L130) | list | `["botkube-plugins-default"]` | Name of group.rbac.authorization.k8s.io the plugin will be bound to. | | [sources.k8s-err-events.botkube/kubernetes.context.rbac.group.static.values](./values.yaml#L130) | list | `["botkube-plugins-default"]` | Name of group.rbac.authorization.k8s.io the plugin will be bound to. | | [sources.k8s-all-events.botkube/kubernetes.context.rbac.group.static.values](./values.yaml#L130) | list | `["botkube-plugins-default"]` | Name of group.rbac.authorization.k8s.io the plugin will be bound to. | | [sources.k8s-recommendation-events.botkube/kubernetes.config.recommendations](./values.yaml#L144) | object | `{"ingress":{"backendServiceValid":true,"tlsSecretValid":true},"pod":{"labelsSet":true,"noLatestImageTag":true}}` | Describes configuration for various recommendation insights. | @@ -86,9 +86,9 @@ Controller for the Botkube Slack app which helps you monitor your Kubernetes clu | [sources.k8s-all-events.botkube/kubernetes.config.filters.objectAnnotationChecker](./values.yaml#L170) | bool | `true` | If true, enables support for `botkube.io/disable` resource annotation. | | [sources.k8s-all-events.botkube/kubernetes.config.filters.nodeEventsChecker](./values.yaml#L172) | bool | `true` | If true, filters out Node-related events that are not important. | | [sources.k8s-all-events.botkube/kubernetes.config.namespaces](./values.yaml#L176) | object | `{"include":[".*"]}` | Describes namespaces for every Kubernetes resources you want to watch or exclude. These namespaces are applied to every resource specified in the resources list. However, every specified resource can override this by using its own namespaces object. | -| [sources.k8s-err-with-logs-events.botkube/kubernetes.config.namespaces.include](./values.yaml#L180) | list | `[".*"]` | Include contains a list of allowed Namespaces. It can also contain regex expressions: `- ".*"` - to specify all Namespaces. | | [sources.k8s-err-events.botkube/kubernetes.config.namespaces.include](./values.yaml#L180) | list | `[".*"]` | Include contains a list of allowed Namespaces. It can also contain regex expressions: `- ".*"` - to specify all Namespaces. | | [sources.k8s-create-events.botkube/kubernetes.config.namespaces.include](./values.yaml#L180) | list | `[".*"]` | Include contains a list of allowed Namespaces. It can also contain regex expressions: `- ".*"` - to specify all Namespaces. | +| [sources.k8s-err-with-logs-events.botkube/kubernetes.config.namespaces.include](./values.yaml#L180) | list | `[".*"]` | Include contains a list of allowed Namespaces. It can also contain regex expressions: `- ".*"` - to specify all Namespaces. | | [sources.k8s-all-events.botkube/kubernetes.config.namespaces.include](./values.yaml#L180) | list | `[".*"]` | Include contains a list of allowed Namespaces. It can also contain regex expressions: `- ".*"` - to specify all Namespaces. | | [sources.k8s-all-events.botkube/kubernetes.config.event](./values.yaml#L190) | object | `{"message":{"exclude":[],"include":[]},"reason":{"exclude":[],"include":[]},"types":["create","delete","error"]}` | Describes event constraints for Kubernetes resources. These constraints are applied for every resource specified in the `resources` list, unless they are overridden by the resource's own `events` object. | | [sources.k8s-all-events.botkube/kubernetes.config.event.types](./values.yaml#L192) | list | `["create","delete","error"]` | Lists all event types to be watched. | @@ -123,117 +123,110 @@ Controller for the Botkube Slack app which helps you monitor your Kubernetes clu | [sources.prometheus.botkube/prometheus.config.log](./values.yaml#L450) | object | `{"level":"info"}` | Logging configuration | | [sources.prometheus.botkube/prometheus.config.log.level](./values.yaml#L452) | string | `"info"` | Log level | | [executors](./values.yaml#L460) | object | See the `values.yaml` file for full object. | Map of executors. Executor contains configuration for running `kubectl` commands. The property name under `executors` is an alias for a given configuration. You can define multiple executor configurations with different names. Key name is used as a binding reference. | -| [executors.k8s-default-tools.kubectl.namespaces.include](./values.yaml#L469) | list | `[".*"]` | List of allowed Kubernetes Namespaces for command execution. It can also contain a regex expressions: `- ".*"` - to specify all Namespaces. | -| [executors.k8s-default-tools.kubectl.namespaces.exclude](./values.yaml#L474) | list | `[]` | List of ignored Kubernetes Namespace. It can also contain a regex expressions: `- "test-.*"` - to specify all Namespaces. | -| [executors.k8s-default-tools.kubectl.enabled](./values.yaml#L476) | bool | `false` | If true, enables `kubectl` commands execution. | -| [executors.k8s-default-tools.kubectl.commands.verbs](./values.yaml#L480) | list | `["api-resources","api-versions","cluster-info","describe","explain","get","logs","top"]` | Configures which `kubectl` methods are allowed. | -| [executors.k8s-default-tools.kubectl.commands.resources](./values.yaml#L482) | list | `["deployments","pods","namespaces","daemonsets","statefulsets","storageclasses","nodes","configmaps","services","ingresses"]` | Configures which K8s resource are allowed. | -| [executors.k8s-default-tools.kubectl.defaultNamespace](./values.yaml#L484) | string | `"default"` | Configures the default Namespace for executing Botkube `kubectl` commands. If not set, uses the 'default'. | -| [executors.k8s-default-tools.kubectl.restrictAccess](./values.yaml#L486) | bool | `false` | If true, enables commands execution from configured channel only. | -| [executors.k8s-default-tools.botkube/helm.enabled](./values.yaml#L492) | bool | `false` | If true, enables `helm` commands execution. | -| [executors.k8s-default-tools.botkube/helm.config.helmDriver](./values.yaml#L497) | string | `"secret"` | Allowed values are configmap, secret, memory. | -| [executors.k8s-default-tools.botkube/helm.config.helmConfigDir](./values.yaml#L499) | string | `"/tmp/helm/"` | Location for storing Helm configuration. | -| [executors.k8s-default-tools.botkube/helm.config.helmCacheDir](./values.yaml#L501) | string | `"/tmp/helm/.cache"` | Location for storing cached files. Must be under the Helm config directory. | -| [executors.k8s-default-tools.botkube/kubectl.config](./values.yaml#L510) | object | See the `values.yaml` file for full object including optional properties related to interactive builder. | Custom kubectl configuration. | -| [aliases](./values.yaml#L535) | object | See the `values.yaml` file for full object. | Custom aliases for given commands. The aliases are replaced with the underlying command before executing it. Aliases can replace a single word or multiple ones. For example, you can define a `k` alias for `kubectl`, or `kgp` for `kubectl get pods`. | -| [existingCommunicationsSecretName](./values.yaml#L555) | 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#L562) | 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.socketSlack.enabled](./values.yaml#L567) | bool | `false` | If true, enables Slack bot. | -| [communications.default-group.socketSlack.channels](./values.yaml#L571) | object | `{"default":{"bindings":{"executors":["k8s-default-tools"],"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#L574) | 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#L577) | list | `["k8s-default-tools"]` | Executors configuration for a given channel. | -| [communications.default-group.socketSlack.channels.default.bindings.sources](./values.yaml#L580) | list | `["k8s-err-events","k8s-recommendation-events"]` | Notification sources configuration for a given channel. | -| [communications.default-group.socketSlack.botToken](./values.yaml#L585) | 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#L588) | string | `""` | Slack app-level token for your own Slack app. [Ref doc](https://api.slack.com/authentication/token-types). | -| [communications.default-group.mattermost.enabled](./values.yaml#L592) | bool | `false` | If true, enables Mattermost bot. | -| [communications.default-group.mattermost.botName](./values.yaml#L594) | string | `"Botkube"` | User in Mattermost which belongs the specified Personal Access token. | -| [communications.default-group.mattermost.url](./values.yaml#L596) | 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#L598) | string | `"MATTERMOST_TOKEN"` | Personal Access token generated by Botkube user. | -| [communications.default-group.mattermost.team](./values.yaml#L600) | string | `"MATTERMOST_TEAM"` | The Mattermost Team name where Botkube is added. | -| [communications.default-group.mattermost.channels](./values.yaml#L604) | object | `{"default":{"bindings":{"executors":["k8s-default-tools"],"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#L608) | 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#L611) | 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#L614) | list | `["k8s-default-tools"]` | Executors configuration for a given channel. | -| [communications.default-group.mattermost.channels.default.bindings.sources](./values.yaml#L617) | list | `["k8s-err-events","k8s-recommendation-events"]` | Notification sources configuration for a given channel. | -| [communications.default-group.teams.enabled](./values.yaml#L624) | bool | `false` | If true, enables MS Teams bot. | -| [communications.default-group.teams.botName](./values.yaml#L626) | string | `"Botkube"` | The Bot name set while registering Bot to MS Teams. | -| [communications.default-group.teams.appID](./values.yaml#L628) | string | `"APPLICATION_ID"` | The Botkube application ID generated while registering Bot to MS Teams. | -| [communications.default-group.teams.appPassword](./values.yaml#L630) | string | `"APPLICATION_PASSWORD"` | The Botkube application password generated while registering Bot to MS Teams. | -| [communications.default-group.teams.bindings.executors](./values.yaml#L633) | list | `["k8s-default-tools"]` | Executor bindings apply to all MS Teams channels where Botkube has access to. | -| [communications.default-group.teams.bindings.sources](./values.yaml#L636) | list | `["k8s-err-events","k8s-recommendation-events"]` | Source bindings apply to all channels which have notification turned on with `@Botkube enable notifications` command. | -| [communications.default-group.teams.messagePath](./values.yaml#L640) | string | `"/bots/teams"` | The path in endpoint URL provided while registering Botkube to MS Teams. | -| [communications.default-group.teams.port](./values.yaml#L642) | int | `3978` | The Service port for bot endpoint on Botkube container. | -| [communications.default-group.discord.enabled](./values.yaml#L647) | bool | `false` | If true, enables Discord bot. | -| [communications.default-group.discord.token](./values.yaml#L649) | string | `"DISCORD_TOKEN"` | Botkube Bot Token. | -| [communications.default-group.discord.botID](./values.yaml#L651) | string | `"DISCORD_BOT_ID"` | Botkube Application Client ID. | -| [communications.default-group.discord.channels](./values.yaml#L655) | object | `{"default":{"bindings":{"executors":["k8s-default-tools"],"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#L659) | 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#L662) | 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#L665) | list | `["k8s-default-tools"]` | Executors configuration for a given channel. | -| [communications.default-group.discord.channels.default.bindings.sources](./values.yaml#L668) | list | `["k8s-err-events","k8s-recommendation-events"]` | Notification sources configuration for a given channel. | -| [communications.default-group.elasticsearch.enabled](./values.yaml#L675) | bool | `false` | If true, enables Elasticsearch. | -| [communications.default-group.elasticsearch.awsSigning.enabled](./values.yaml#L679) | 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#L681) | string | `"us-east-1"` | AWS region where Elasticsearch is deployed. | -| [communications.default-group.elasticsearch.awsSigning.roleArn](./values.yaml#L683) | 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#L685) | string | `"ELASTICSEARCH_ADDRESS"` | The server URL, e.g https://example.com:9243 | -| [communications.default-group.elasticsearch.username](./values.yaml#L687) | string | `"ELASTICSEARCH_USERNAME"` | Basic Auth username. | -| [communications.default-group.elasticsearch.password](./values.yaml#L689) | string | `"ELASTICSEARCH_PASSWORD"` | Basic Auth password. | -| [communications.default-group.elasticsearch.skipTLSVerify](./values.yaml#L692) | 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#L696) | 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#L699) | string | `"botkube"` | Configures Elasticsearch index settings. | -| [communications.default-group.elasticsearch.indices.default.bindings.sources](./values.yaml#L705) | list | `["k8s-err-events","k8s-recommendation-events"]` | Notification sources configuration for a given index. | -| [communications.default-group.webhook.enabled](./values.yaml#L712) | bool | `false` | If true, enables Webhook. | -| [communications.default-group.webhook.url](./values.yaml#L714) | string | `"WEBHOOK_URL"` | The Webhook URL, e.g.: https://example.com:80 | -| [communications.default-group.webhook.bindings.sources](./values.yaml#L717) | list | `["k8s-err-events","k8s-recommendation-events"]` | Notification sources configuration for the webhook. | -| [communications.default-group.slack](./values.yaml#L727) | object | See the `values.yaml` file for full object. | Settings for deprecated Slack integration. **DEPRECATED:** Legacy Slack integration has been deprecated and removed from the Slack App Directory. Use `socketSlack` instead. Read more here: https://docs.botkube.io/installation/slack/ | -| [settings.clusterName](./values.yaml#L745) | string | `"not-configured"` | Cluster name to differentiate incoming messages. | -| [settings.lifecycleServer](./values.yaml#L748) | object | `{"enabled":true,"port":2113}` | Server configuration which exposes functionality related to the app lifecycle. | -| [settings.healthPort](./values.yaml#L751) | int | `2114` | | -| [settings.upgradeNotifier](./values.yaml#L753) | bool | `true` | If true, notifies about new Botkube releases. | -| [settings.log.level](./values.yaml#L757) | string | `"info"` | Sets one of the log levels. Allowed values: `info`, `warn`, `debug`, `error`, `fatal`, `panic`. | -| [settings.log.disableColors](./values.yaml#L759) | bool | `false` | If true, disable ANSI colors in logging. | -| [settings.systemConfigMap](./values.yaml#L762) | object | `{"name":"botkube-system"}` | Botkube's system ConfigMap where internal data is stored. | -| [settings.persistentConfig](./values.yaml#L767) | 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#L782) | bool | `false` | If true, specify cert path in `config.ssl.cert` property or K8s Secret in `config.ssl.existingSecretName`. | -| [ssl.existingSecretName](./values.yaml#L788) | string | `""` | Using existing SSL Secret. It MUST be in `botkube` Namespace. | -| [ssl.cert](./values.yaml#L791) | string | `""` | SSL Certificate file e.g certs/my-cert.crt. | -| [service](./values.yaml#L794) | object | `{"name":"metrics","port":2112,"targetPort":2112}` | Configures Service settings for ServiceMonitor CR. | -| [ingress](./values.yaml#L801) | 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#L812) | 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#L822) | object | `{}` | Extra annotations to pass to the Botkube Deployment. | -| [extraAnnotations](./values.yaml#L829) | object | `{}` | Extra annotations to pass to the Botkube Pod. | -| [extraLabels](./values.yaml#L831) | object | `{}` | Extra labels to pass to the Botkube Pod. | -| [priorityClassName](./values.yaml#L833) | string | `""` | Priority class name for the Botkube Pod. | -| [nameOverride](./values.yaml#L836) | string | `""` | Fully override "botkube.name" template. | -| [fullnameOverride](./values.yaml#L838) | string | `""` | Fully override "botkube.fullname" template. | -| [resources](./values.yaml#L844) | 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#L856) | list | `[{"name":"LOG_LEVEL_SOURCE_BOTKUBE_KUBERNETES","value":"debug"}]` | 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#L870) | 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#L885) | 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#L903) | object | `{}` | Node labels for Botkube Pod assignment. [Ref doc](https://kubernetes.io/docs/user-guide/node-selection/). | -| [tolerations](./values.yaml#L907) | list | `[]` | Tolerations for Botkube Pod assignment. [Ref doc](https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/). | -| [affinity](./values.yaml#L911) | object | `{}` | Affinity for Botkube Pod assignment. [Ref doc](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity). | -| [serviceAccount.create](./values.yaml#L915) | bool | `true` | If true, a ServiceAccount is automatically created. | -| [serviceAccount.name](./values.yaml#L918) | string | `""` | The name of the service account to use. If not set, a name is generated using the fullname template. | -| [serviceAccount.annotations](./values.yaml#L920) | object | `{}` | Extra annotations for the ServiceAccount. | -| [extraObjects](./values.yaml#L923) | list | `[]` | Extra Kubernetes resources to create. Helm templating is allowed as it is evaluated before creating the resources. | -| [analytics.disable](./values.yaml#L951) | bool | `false` | If true, sending anonymous analytics is disabled. To learn what date we collect, see [Privacy Policy](https://docs.botkube.io/privacy#privacy-policy). | -| [configWatcher.enabled](./values.yaml#L956) | bool | `true` | If true, restarts the Botkube Pod on config changes. | -| [configWatcher.tmpDir](./values.yaml#L958) | string | `"/tmp/watched-cfg/"` | Directory, where watched configuration resources are stored. | -| [configWatcher.initialSyncTimeout](./values.yaml#L961) | 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#L964) | string | `"ghcr.io"` | Config watcher image registry. | -| [configWatcher.image.repository](./values.yaml#L966) | string | `"kubeshop/k8s-sidecar"` | Config watcher image repository. | -| [configWatcher.image.tag](./values.yaml#L968) | string | `"ignore-initial-events"` | Config watcher image tag. | -| [configWatcher.image.pullPolicy](./values.yaml#L970) | string | `"IfNotPresent"` | Config watcher image pull policy. | -| [plugins](./values.yaml#L973) | object | `{"cacheDir":"/tmp","repositories":{"botkube":{"url":"https://github.com/kubeshop/botkube/releases/download/v9.99.9-dev/plugins-index.yaml"}}}` | Configuration for Botkube executors and sources plugins. | -| [plugins.cacheDir](./values.yaml#L975) | string | `"/tmp"` | Directory, where downloaded plugins are cached. | -| [plugins.repositories](./values.yaml#L977) | object | `{"botkube":{"url":"https://github.com/kubeshop/botkube/releases/download/v9.99.9-dev/plugins-index.yaml"}}` | List of plugins repositories. | -| [plugins.repositories.botkube](./values.yaml#L979) | object | `{"url":"https://github.com/kubeshop/botkube/releases/download/v9.99.9-dev/plugins-index.yaml"}` | This repository serves officially supported Botkube plugins. | -| [config](./values.yaml#L983) | object | `{"provider":{"apiKey":"","endpoint":"https://api.botkube.io/graphql","identifier":""}}` | Configuration for synchronizing Botkube configuration. | -| [config.provider](./values.yaml#L985) | object | `{"apiKey":"","endpoint":"https://api.botkube.io/graphql","identifier":""}` | Base provider definition. | -| [config.provider.identifier](./values.yaml#L988) | string | `""` | Unique identifier for remote Botkube settings. If set to an empty string, Botkube won't fetch remote configuration. | -| [config.provider.endpoint](./values.yaml#L990) | string | `"https://api.botkube.io/graphql"` | Endpoint to fetch Botkube settings from. | -| [config.provider.apiKey](./values.yaml#L992) | string | `""` | Key passed as a `X-API-Key` header to the provider's endpoint. | +| [executors.k8s-default-tools.botkube/helm.enabled](./values.yaml#L466) | bool | `false` | If true, enables `helm` commands execution. | +| [executors.k8s-default-tools.botkube/helm.config.helmDriver](./values.yaml#L471) | string | `"secret"` | Allowed values are configmap, secret, memory. | +| [executors.k8s-default-tools.botkube/helm.config.helmConfigDir](./values.yaml#L473) | string | `"/tmp/helm/"` | Location for storing Helm configuration. | +| [executors.k8s-default-tools.botkube/helm.config.helmCacheDir](./values.yaml#L475) | string | `"/tmp/helm/.cache"` | Location for storing cached files. Must be under the Helm config directory. | +| [executors.k8s-default-tools.botkube/kubectl.config](./values.yaml#L484) | object | See the `values.yaml` file for full object including optional properties related to interactive builder. | Custom kubectl configuration. | +| [aliases](./values.yaml#L509) | object | See the `values.yaml` file for full object. | Custom aliases for given commands. The aliases are replaced with the underlying command before executing it. Aliases can replace a single word or multiple ones. For example, you can define a `k` alias for `kubectl`, or `kgp` for `kubectl get pods`. | +| [existingCommunicationsSecretName](./values.yaml#L529) | 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#L536) | 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.socketSlack.enabled](./values.yaml#L541) | bool | `false` | If true, enables Slack bot. | +| [communications.default-group.socketSlack.channels](./values.yaml#L545) | object | `{"default":{"bindings":{"executors":["k8s-default-tools"],"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#L548) | 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#L551) | list | `["k8s-default-tools"]` | Executors configuration for a given channel. | +| [communications.default-group.socketSlack.channels.default.bindings.sources](./values.yaml#L554) | list | `["k8s-err-events","k8s-recommendation-events"]` | Notification sources configuration for a given channel. | +| [communications.default-group.socketSlack.botToken](./values.yaml#L559) | 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#L562) | string | `""` | Slack app-level token for your own Slack app. [Ref doc](https://api.slack.com/authentication/token-types). | +| [communications.default-group.mattermost.enabled](./values.yaml#L566) | bool | `false` | If true, enables Mattermost bot. | +| [communications.default-group.mattermost.botName](./values.yaml#L568) | string | `"Botkube"` | User in Mattermost which belongs the specified Personal Access token. | +| [communications.default-group.mattermost.url](./values.yaml#L570) | 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#L572) | string | `"MATTERMOST_TOKEN"` | Personal Access token generated by Botkube user. | +| [communications.default-group.mattermost.team](./values.yaml#L574) | string | `"MATTERMOST_TEAM"` | The Mattermost Team name where Botkube is added. | +| [communications.default-group.mattermost.channels](./values.yaml#L578) | object | `{"default":{"bindings":{"executors":["k8s-default-tools"],"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#L582) | 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#L585) | 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#L588) | list | `["k8s-default-tools"]` | Executors configuration for a given channel. | +| [communications.default-group.mattermost.channels.default.bindings.sources](./values.yaml#L591) | list | `["k8s-err-events","k8s-recommendation-events"]` | Notification sources configuration for a given channel. | +| [communications.default-group.teams.enabled](./values.yaml#L598) | bool | `false` | If true, enables MS Teams bot. | +| [communications.default-group.teams.botName](./values.yaml#L600) | string | `"Botkube"` | The Bot name set while registering Bot to MS Teams. | +| [communications.default-group.teams.appID](./values.yaml#L602) | string | `"APPLICATION_ID"` | The Botkube application ID generated while registering Bot to MS Teams. | +| [communications.default-group.teams.appPassword](./values.yaml#L604) | string | `"APPLICATION_PASSWORD"` | The Botkube application password generated while registering Bot to MS Teams. | +| [communications.default-group.teams.bindings.executors](./values.yaml#L607) | list | `["k8s-default-tools"]` | Executor bindings apply to all MS Teams channels where Botkube has access to. | +| [communications.default-group.teams.bindings.sources](./values.yaml#L610) | list | `["k8s-err-events","k8s-recommendation-events"]` | Source bindings apply to all channels which have notification turned on with `@Botkube enable notifications` command. | +| [communications.default-group.teams.messagePath](./values.yaml#L614) | string | `"/bots/teams"` | The path in endpoint URL provided while registering Botkube to MS Teams. | +| [communications.default-group.teams.port](./values.yaml#L616) | int | `3978` | The Service port for bot endpoint on Botkube container. | +| [communications.default-group.discord.enabled](./values.yaml#L621) | bool | `false` | If true, enables Discord bot. | +| [communications.default-group.discord.token](./values.yaml#L623) | string | `"DISCORD_TOKEN"` | Botkube Bot Token. | +| [communications.default-group.discord.botID](./values.yaml#L625) | string | `"DISCORD_BOT_ID"` | Botkube Application Client ID. | +| [communications.default-group.discord.channels](./values.yaml#L629) | object | `{"default":{"bindings":{"executors":["k8s-default-tools"],"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#L633) | 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#L636) | 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#L639) | list | `["k8s-default-tools"]` | Executors configuration for a given channel. | +| [communications.default-group.discord.channels.default.bindings.sources](./values.yaml#L642) | list | `["k8s-err-events","k8s-recommendation-events"]` | Notification sources configuration for a given channel. | +| [communications.default-group.elasticsearch.enabled](./values.yaml#L649) | bool | `false` | If true, enables Elasticsearch. | +| [communications.default-group.elasticsearch.awsSigning.enabled](./values.yaml#L653) | 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#L655) | string | `"us-east-1"` | AWS region where Elasticsearch is deployed. | +| [communications.default-group.elasticsearch.awsSigning.roleArn](./values.yaml#L657) | 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#L659) | string | `"ELASTICSEARCH_ADDRESS"` | The server URL, e.g https://example.com:9243 | +| [communications.default-group.elasticsearch.username](./values.yaml#L661) | string | `"ELASTICSEARCH_USERNAME"` | Basic Auth username. | +| [communications.default-group.elasticsearch.password](./values.yaml#L663) | string | `"ELASTICSEARCH_PASSWORD"` | Basic Auth password. | +| [communications.default-group.elasticsearch.skipTLSVerify](./values.yaml#L666) | 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#L670) | 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#L673) | string | `"botkube"` | Configures Elasticsearch index settings. | +| [communications.default-group.elasticsearch.indices.default.bindings.sources](./values.yaml#L679) | list | `["k8s-err-events","k8s-recommendation-events"]` | Notification sources configuration for a given index. | +| [communications.default-group.webhook.enabled](./values.yaml#L686) | bool | `false` | If true, enables Webhook. | +| [communications.default-group.webhook.url](./values.yaml#L688) | string | `"WEBHOOK_URL"` | The Webhook URL, e.g.: https://example.com:80 | +| [communications.default-group.webhook.bindings.sources](./values.yaml#L691) | list | `["k8s-err-events","k8s-recommendation-events"]` | Notification sources configuration for the webhook. | +| [communications.default-group.slack](./values.yaml#L701) | object | See the `values.yaml` file for full object. | Settings for deprecated Slack integration. **DEPRECATED:** Legacy Slack integration has been deprecated and removed from the Slack App Directory. Use `socketSlack` instead. Read more here: https://docs.botkube.io/installation/slack/ | +| [settings.clusterName](./values.yaml#L719) | string | `"not-configured"` | Cluster name to differentiate incoming messages. | +| [settings.lifecycleServer](./values.yaml#L722) | object | `{"enabled":true,"port":2113}` | Server configuration which exposes functionality related to the app lifecycle. | +| [settings.healthPort](./values.yaml#L725) | int | `2114` | | +| [settings.upgradeNotifier](./values.yaml#L727) | bool | `true` | If true, notifies about new Botkube releases. | +| [settings.log.level](./values.yaml#L731) | string | `"info"` | Sets one of the log levels. Allowed values: `info`, `warn`, `debug`, `error`, `fatal`, `panic`. | +| [settings.log.disableColors](./values.yaml#L733) | bool | `false` | If true, disable ANSI colors in logging. | +| [settings.systemConfigMap](./values.yaml#L736) | object | `{"name":"botkube-system"}` | Botkube's system ConfigMap where internal data is stored. | +| [settings.persistentConfig](./values.yaml#L741) | 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#L756) | bool | `false` | If true, specify cert path in `config.ssl.cert` property or K8s Secret in `config.ssl.existingSecretName`. | +| [ssl.existingSecretName](./values.yaml#L762) | string | `""` | Using existing SSL Secret. It MUST be in `botkube` Namespace. | +| [ssl.cert](./values.yaml#L765) | string | `""` | SSL Certificate file e.g certs/my-cert.crt. | +| [service](./values.yaml#L768) | object | `{"name":"metrics","port":2112,"targetPort":2112}` | Configures Service settings for ServiceMonitor CR. | +| [ingress](./values.yaml#L775) | 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#L786) | 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#L796) | object | `{}` | Extra annotations to pass to the Botkube Deployment. | +| [extraAnnotations](./values.yaml#L803) | object | `{}` | Extra annotations to pass to the Botkube Pod. | +| [extraLabels](./values.yaml#L805) | object | `{}` | Extra labels to pass to the Botkube Pod. | +| [priorityClassName](./values.yaml#L807) | string | `""` | Priority class name for the Botkube Pod. | +| [nameOverride](./values.yaml#L810) | string | `""` | Fully override "botkube.name" template. | +| [fullnameOverride](./values.yaml#L812) | string | `""` | Fully override "botkube.fullname" template. | +| [resources](./values.yaml#L818) | 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#L830) | list | `[{"name":"LOG_LEVEL_SOURCE_BOTKUBE_KUBERNETES","value":"debug"}]` | 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#L844) | 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#L859) | 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#L877) | object | `{}` | Node labels for Botkube Pod assignment. [Ref doc](https://kubernetes.io/docs/user-guide/node-selection/). | +| [tolerations](./values.yaml#L881) | list | `[]` | Tolerations for Botkube Pod assignment. [Ref doc](https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/). | +| [affinity](./values.yaml#L885) | object | `{}` | Affinity for Botkube Pod assignment. [Ref doc](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity). | +| [serviceAccount.create](./values.yaml#L889) | bool | `true` | If true, a ServiceAccount is automatically created. | +| [serviceAccount.name](./values.yaml#L892) | string | `""` | The name of the service account to use. If not set, a name is generated using the fullname template. | +| [serviceAccount.annotations](./values.yaml#L894) | object | `{}` | Extra annotations for the ServiceAccount. | +| [extraObjects](./values.yaml#L897) | list | `[]` | Extra Kubernetes resources to create. Helm templating is allowed as it is evaluated before creating the resources. | +| [analytics.disable](./values.yaml#L925) | bool | `false` | If true, sending anonymous analytics is disabled. To learn what date we collect, see [Privacy Policy](https://docs.botkube.io/privacy#privacy-policy). | +| [configWatcher.enabled](./values.yaml#L930) | bool | `true` | If true, restarts the Botkube Pod on config changes. | +| [configWatcher.tmpDir](./values.yaml#L932) | string | `"/tmp/watched-cfg/"` | Directory, where watched configuration resources are stored. | +| [configWatcher.initialSyncTimeout](./values.yaml#L935) | 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#L938) | string | `"ghcr.io"` | Config watcher image registry. | +| [configWatcher.image.repository](./values.yaml#L940) | string | `"kubeshop/k8s-sidecar"` | Config watcher image repository. | +| [configWatcher.image.tag](./values.yaml#L942) | string | `"ignore-initial-events"` | Config watcher image tag. | +| [configWatcher.image.pullPolicy](./values.yaml#L944) | string | `"IfNotPresent"` | Config watcher image pull policy. | +| [plugins](./values.yaml#L947) | object | `{"cacheDir":"/tmp","repositories":{"botkube":{"url":"https://github.com/kubeshop/botkube/releases/download/v9.99.9-dev/plugins-index.yaml"}}}` | Configuration for Botkube executors and sources plugins. | +| [plugins.cacheDir](./values.yaml#L949) | string | `"/tmp"` | Directory, where downloaded plugins are cached. | +| [plugins.repositories](./values.yaml#L951) | object | `{"botkube":{"url":"https://github.com/kubeshop/botkube/releases/download/v9.99.9-dev/plugins-index.yaml"}}` | List of plugins repositories. | +| [plugins.repositories.botkube](./values.yaml#L953) | object | `{"url":"https://github.com/kubeshop/botkube/releases/download/v9.99.9-dev/plugins-index.yaml"}` | This repository serves officially supported Botkube plugins. | +| [config](./values.yaml#L957) | object | `{"provider":{"apiKey":"","endpoint":"https://api.botkube.io/graphql","identifier":""}}` | Configuration for synchronizing Botkube configuration. | +| [config.provider](./values.yaml#L959) | object | `{"apiKey":"","endpoint":"https://api.botkube.io/graphql","identifier":""}` | Base provider definition. | +| [config.provider.identifier](./values.yaml#L962) | string | `""` | Unique identifier for remote Botkube settings. If set to an empty string, Botkube won't fetch remote configuration. | +| [config.provider.endpoint](./values.yaml#L964) | string | `"https://api.botkube.io/graphql"` | Endpoint to fetch Botkube settings from. | +| [config.provider.apiKey](./values.yaml#L966) | string | `""` | Key passed as a `X-API-Key` header to the provider's endpoint. | ### AWS IRSA on EKS support diff --git a/helm/botkube/e2e-test-values.yaml b/helm/botkube/e2e-test-values.yaml index 114760ace..5ea6be115 100644 --- a/helm/botkube/e2e-test-values.yaml +++ b/helm/botkube/e2e-test-values.yaml @@ -7,9 +7,6 @@ rbac: - apiGroups: [ "*" ] resources: [ "*" ] verbs: [ "get", "watch", "list" ] # defaults - - apiGroups: [ "" ] - resources: [ "services" ] - verbs: [ "patch" ] # needed for label action staticGroupName: &static-group-name "botkube-plugins-default" communications: @@ -22,16 +19,14 @@ communications: name: "" # Tests will override this temporarily bindings: executors: - - k8s-default-tools - - kubectl-wait-cmd + - kubectl-first-channel-cmd - kubectl-exec-cmd - - kubectl-allow-all - - plugin-based + - other-plugins sources: - k8s-events - k8s-annotated-cm-delete - k8s-pod-create-events - - plugin-based + - other-plugins 'secondary': name: "" # Tests will override this temporarily notification: @@ -50,16 +45,14 @@ communications: id: "" # Tests will override this channel ID temporarily bindings: executors: - - k8s-default-tools - - kubectl-wait-cmd + - kubectl-first-channel-cmd - kubectl-exec-cmd - - kubectl-allow-all - - plugin-based + - other-plugins sources: - k8s-events - k8s-annotated-cm-delete - k8s-pod-create-events - - plugin-based + - other-plugins 'secondary': id: "" # Tests will override this channel ID temporarily notification: @@ -77,24 +70,16 @@ sources: displayName: "K8s recommendations" 'botkube/kubernetes': context: &defaultPluginContext - defaultNamespace: "default" - # -- RBAC configuration for this plugin. rbac: - # -- Static impersonation for a given username and groups. group: type: Static - # -- Prefix that will be applied to .static.value[*]. - prefix: "" static: - # -- Name of group.rbac.authorization.k8s.io the plugin will be bound to. values: [*static-group-name] # "botkube-plugins-read-only" is the default user: type: Static - # -- Prefix that will be applied to .static.value[*]. - prefix: "" static: - # -- Name of user.rbac.authorization.k8s.io the plugin will be bound to. value: *static-group-name + enabled: true config: log: @@ -190,7 +175,7 @@ sources: types: - update - 'plugin-based': + 'other-plugins': displayName: "K8s ConfigMaps changes" botkube/cm-watcher: context: *defaultPluginContext @@ -203,61 +188,49 @@ sources: executors: 'k8s-default-tools': - kubectl: + botkube/kubectl: enabled: true - namespaces: - include: - - botkube - - default - 'kubectl-wait-cmd': - kubectl: + 'kubectl-first-channel-cmd': + botkube/kubectl: enabled: true - namespaces: - include: - - botkube - - default - commands: - verbs: [ "wait" ] - restrictAccess: false + context: + rbac: + group: + type: Static + static: + values: [ "kubectl-first-channel" ] 'kubectl-exec-cmd': - kubectl: + botkube/kubectl: enabled: false - namespaces: - include: - - botkube - - default - commands: - verbs: [ "exec" ] - restrictAccess: false - 'kubectl-allow-all': - kubectl: - enabled: true - namespaces: - include: - - ".*" - commands: - verbs: [ "get" ] - resources: [ "deployments" ] + context: + rbac: + group: + type: Static + static: + # 'exec' verb perms on 'botkube' and 'default' namespaces + values: [ "kc-exec-only" ] 'kubectl-not-bound-to-any-channel': - kubectl: + botkube/kubectl: enabled: true - namespaces: - include: - - ".*" - commands: - verbs: [ "port-forward" ] - resources: [ "deployments" ] - + context: + rbac: + group: + type: Static + static: + # deployments port-forward across all namespaces + values: [ "kubectl-first-channel" ] 'kubectl-with-svc-label-perms': - kubectl: + botkube/kubectl: enabled: true - namespaces: - include: [ ".*" ] - commands: - verbs: [ "label" ] - resources: [ "services" ] + context: + rbac: + group: + type: Static + static: + # service labeling across all namespaces + values: [ "kc-label-svc-all" ] - 'plugin-based': + 'other-plugins': botkube/echo@v1.0.1-devel: enabled: true config: @@ -327,3 +300,118 @@ extraAnnotations: extraEnv: - name: LOG_LEVEL_SOURCE_BOTKUBE_KUBERNETES value: debug + +extraObjects: + +# Group 'kubectl-first-channel': permissions for kubectl for first channel +## namespace scoped permissions +- apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: &kubectl-wait + name: kubectl-first-channel-namespaced-perms + labels: + app.kubernetes.io/instance: botkube-e2e-test + rules: + - apiGroups: [ "apps" ] + resources: [ "deployments" ] + verbs: [ "get","watch","list" ] + - apiGroups: [""] + resources: ["configmaps", "pods"] + verbs: ["get", "watch", "list"] +- apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + <<: *kubectl-wait + namespace: botkube + roleRef: &kubectl-wait-role + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kubectl-first-channel-namespaced-perms + subjects: &kubectl-first-channel-subject + - kind: Group + name: kubectl-first-channel + apiGroup: rbac.authorization.k8s.io +- apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + <<: *kubectl-wait + namespace: default + roleRef: *kubectl-wait-role + subjects: *kubectl-first-channel-subject + +### cluster permissions +- apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: &kubectl-deploy-all-meta + name: kc-first-channel-cluster-perms + labels: + app.kubernetes.io/instance: botkube-e2e-test + rules: + - apiGroups: [ "apps" ] + resources: [ "deployments" ] + verbs: [ "get", "list" ] +- apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: *kubectl-deploy-all-meta + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kc-first-channel-cluster-perms + subjects: *kubectl-first-channel-subject + +# Group 'kc-exec-only' +## exec only for default and botkube namespaces: +- apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: &kc-exec-only-meta + name: kc-exec-only + labels: + app.kubernetes.io/instance: botkube-e2e-test + rules: + - apiGroups: [""] + resources: ["pods/exec"] + verbs: ["create"] +- apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + <<: *kc-exec-only-meta + namespace: botkube + roleRef: &kc-exec-only-role + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kc-exec-only + subjects: &kc-exec-only-subject + - kind: Group + name: kc-exec-only + apiGroup: rbac.authorization.k8s.io +- apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + <<: *kc-exec-only-meta + namespace: default + roleRef: *kc-exec-only-role + subjects: *kc-exec-only-subject + +# Group 'kc-label-svc-all': +## namespace scoped permissions +- apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: &kc-label-svc-all-meta + name: kc-label-svc-all + labels: + app.kubernetes.io/instance: botkube-e2e-test + rules: + - apiGroups: [ "" ] + resources: [ "services" ] + verbs: [ "get", "patch" ] +- apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: *kc-label-svc-all-meta + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kc-label-svc-all + subjects: + - kind: Group + name: kc-label-svc-all + apiGroup: rbac.authorization.k8s.io diff --git a/helm/botkube/values.yaml b/helm/botkube/values.yaml index 847dd59d2..aab5fc9c6 100644 --- a/helm/botkube/values.yaml +++ b/helm/botkube/values.yaml @@ -459,32 +459,6 @@ sources: ## Format: executors.{alias} executors: k8s-default-tools: - ## Built-in Kubectl executor configuration. - ## DEPRECATED: The botkube/kubectl plugin version should be used instead. If both are enabled the plugin version takes the precedences. - kubectl: - namespaces: - # -- List of allowed Kubernetes Namespaces for command execution. - # It can also contain a regex expressions: - # `- ".*"` - to specify all Namespaces. - include: - - ".*" - # -- List of ignored Kubernetes Namespace. - # It can also contain a regex expressions: - # `- "test-.*"` - to specify all Namespaces. - exclude: [] - # -- If true, enables `kubectl` commands execution. - enabled: false - ## List of allowed `kubectl` commands. - commands: - # -- Configures which `kubectl` methods are allowed. - verbs: ["api-resources", "api-versions", "cluster-info", "describe", "explain", "get", "logs", "top"] - # -- Configures which K8s resource are allowed. - resources: ["deployments", "pods", "namespaces", "daemonsets", "statefulsets", "storageclasses", "nodes", "configmaps", "services", "ingresses"] - # -- Configures the default Namespace for executing Botkube `kubectl` commands. If not set, uses the 'default'. - defaultNamespace: default - # -- If true, enables commands execution from configured channel only. - restrictAccess: false - ## Helm executor configuration ## Plugin name syntax: /[@]. If version is not provided, the latest version from repository is used. botkube/helm: diff --git a/internal/executor/helm/executor.go b/internal/executor/helm/executor.go index 84ecc1f71..a7843cde6 100644 --- a/internal/executor/helm/executor.go +++ b/internal/executor/helm/executor.go @@ -107,11 +107,11 @@ func (e *Executor) Execute(ctx context.Context, in executor.ExecuteInput) (execu kubeConfigPath, deleteFn, err := pluginx.PersistKubeConfig(ctx, in.Context.KubeConfig) if err != nil { - return executor.ExecuteOutput{}, fmt.Errorf("while writing kubeConfig file: %w", err) + return executor.ExecuteOutput{}, fmt.Errorf("while writing kubeconfig file: %w", err) } defer func() { if deleteErr := deleteFn(ctx); deleteErr != nil { - fmt.Fprintf(os.Stderr, "failed to delete cube config file %s: %v", kubeConfigPath, deleteErr) + fmt.Fprintf(os.Stderr, "failed to delete kubeconfig file %s: %v", kubeConfigPath, deleteErr) } }() diff --git a/internal/executor/kubectl/executor.go b/internal/executor/kubectl/executor.go index 36b3414f0..7d2541435 100644 --- a/internal/executor/kubectl/executor.go +++ b/internal/executor/kubectl/executor.go @@ -110,11 +110,11 @@ func (e *Executor) Execute(ctx context.Context, in executor.ExecuteInput) (execu kubeConfigPath, deleteFn, err := pluginx.PersistKubeConfig(ctx, in.Context.KubeConfig) if err != nil { - return executor.ExecuteOutput{}, fmt.Errorf("while writing kubeConfig file: %w", err) + return executor.ExecuteOutput{}, fmt.Errorf("while writing kubeconfig file: %w", err) } defer func() { if deleteErr := deleteFn(ctx); deleteErr != nil { - log.Errorf("failed to delete cube config file %s: %w", kubeConfigPath, deleteErr) + log.Errorf("failed to delete kubeconfig file %s: %w", kubeConfigPath, deleteErr) } }() diff --git a/internal/plugin/kubeconfig.go b/internal/plugin/kubeconfig.go index 6a2e4c940..eb1713243 100644 --- a/internal/plugin/kubeconfig.go +++ b/internal/plugin/kubeconfig.go @@ -9,7 +9,8 @@ import ( ) const ( - kubeconfigDefaultValue = "default" + kubeconfigDefaultValue = "default" + kubeconfigDefaultNamespace = "default" ) type KubeConfigInput struct { @@ -39,7 +40,7 @@ func GenerateKubeConfig(restCfg *rest.Config, pluginCtx config.PluginContext, in Name: kubeconfigDefaultValue, Context: clientcmdapi.Context{ Cluster: kubeconfigDefaultValue, - Namespace: pluginCtx.DefaultNamespace, + Namespace: kubeconfigDefaultNamespace, AuthInfo: kubeconfigDefaultValue, }, }, diff --git a/internal/source/dispatcher.go b/internal/source/dispatcher.go index a9f896ee1..688d7a44e 100644 --- a/internal/source/dispatcher.go +++ b/internal/source/dispatcher.go @@ -204,8 +204,14 @@ func (d *Dispatcher) dispatchMsg(ctx context.Context, event source.Event, dispat return } for _, act := range actions { - d.log.Infof("Executing action %q (command: %q)...", act.DisplayName, act.Command) + log := d.log.WithFields(logrus.Fields{ + "name": act.DisplayName, + "command": act.Command, + }) + log.Infof("Executing automated action...") genericMsg := d.actionProvider.ExecuteAction(ctx, act) + log.WithField("message", fmt.Sprintf("%+v", genericMsg)).Debug("Automated action executed. Printing output message...") + for _, n := range d.getBotNotifiers(dispatch) { go func(n notifier.Bot) { defer analytics.ReportPanicIfOccurs(d.log, d.reporter) diff --git a/pkg/bot/interactive/help.go b/pkg/bot/interactive/help.go index 4ba3a47bb..f28dd420d 100644 --- a/pkg/bot/interactive/help.go +++ b/pkg/bot/interactive/help.go @@ -3,9 +3,10 @@ package interactive import ( "fmt" + "golang.org/x/exp/slices" + "github.com/kubeshop/botkube/pkg/api" "github.com/kubeshop/botkube/pkg/config" - "github.com/kubeshop/botkube/pkg/formatx" ) // RunCommandName defines the button name for the run commands. @@ -44,7 +45,6 @@ func (h *HelpMessage) Build() CoreMessage { h.configSections, h.executorSections, h.pluginHelpSections, - h.filters, h.feedback, h.footer, } @@ -88,19 +88,6 @@ func (h *HelpMessage) ping() []api.Section { } } -func (h *HelpMessage) filters() []api.Section { - return []api.Section{ - { - Base: api.Base{ - Header: "Filters (advanced)", - Body: api.Body{ - Plaintext: "You can extend Botkube functionality by writing additional filters that can check resource specs, validate some checks and add messages to the Event struct. Learn more at https://docs.botkube.io/filters", - }, - }, - }, - } -} - func (h *HelpMessage) feedback() []api.Section { return []api.Section{ { @@ -186,63 +173,14 @@ func (h *HelpMessage) configSections() []api.Section { } func (h *HelpMessage) executorSections() []api.Section { - if h.platform.IsInteractive() { - return []api.Section{ - { - Base: api.Base{ - Header: "Interactive kubectl - no typing!", - Description: "Build kubectl commands interactively", - }, - Buttons: []api.Button{ - h.btnBuilder.ForCommandWithDescCmd("kubectl", "kubectl", api.ButtonStylePrimary), - }, - }, - { - Base: api.Base{ - Description: "To list all enabled executors", - }, - Buttons: []api.Button{ - h.btnBuilder.ForCommandWithDescCmd("List executors", "list executors"), - }, - }, - { - Base: api.Base{ - Description: "To list all command aliases", - }, - Buttons: []api.Button{ - h.btnBuilder.ForCommandWithDescCmd("List aliases", "list aliases"), - }, - }, - } - } - - // without the kubectl command builder return []api.Section{ { Base: api.Base{ - Header: "Run kubectl commands (if enabled)", - Description: fmt.Sprintf("You can run kubectl commands directly from %s!", formatx.ToTitle(h.platform)), - }, - Buttons: []api.Button{ - h.btnBuilder.ForCommandWithDescCmd(RunCommandName, "kubectl get services"), - h.btnBuilder.ForCommandWithDescCmd(RunCommandName, "kubectl get pods"), - h.btnBuilder.ForCommandWithDescCmd(RunCommandName, "kubectl get deployments"), - }, - }, - { - Base: api.Base{ - Description: "To list all enabled executors", + Header: "Manage executors", }, Buttons: []api.Button{ h.btnBuilder.ForCommandWithDescCmd("List executors", "list executors"), - }, - }, - { - Base: api.Base{ - Description: "To list all command aliases", - }, - Buttons: []api.Button{ - h.btnBuilder.ForCommandWithDescCmd("List aliases", "list aliases"), + h.btnBuilder.ForCommandWithDescCmd("List executor aliases", "list aliases"), }, }, } @@ -250,18 +188,16 @@ func (h *HelpMessage) executorSections() []api.Section { func (h *HelpMessage) pluginHelpSections() []api.Section { var out []api.Section + + slices.Sort(h.enabledPluginExecutors) // to make the order predictable for testing + for _, name := range h.enabledPluginExecutors { helpFn, found := pluginHelpProvider[name] if !found { continue } - platformName := h.platform - if h.platform == config.SocketSlackCommPlatformIntegration { - platformName = "slack" // normalize the SocketSlack to Slack - } - - helpSection := helpFn(formatx.ToTitle(platformName), h.btnBuilder) + helpSection := helpFn(h.platform, h.btnBuilder) out = append(out, helpSection) } return out diff --git a/pkg/bot/interactive/markdown_test.go b/pkg/bot/interactive/markdown_test.go index ff7d89c5d..0336cda9a 100644 --- a/pkg/bot/interactive/markdown_test.go +++ b/pkg/bot/interactive/markdown_test.go @@ -163,7 +163,7 @@ func TestInteractiveMessageToMarkdown(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // given - given := NewHelpMessage("platform", "testing", nil).Build() + given := NewHelpMessage("platform", "testing", []string{"botkube/kubectl"}).Build() given.ReplaceBotNamePlaceholder("@Botkube") // when diff --git a/pkg/bot/interactive/plaintext_test.go b/pkg/bot/interactive/plaintext_test.go index 82ef45caa..c7e439b00 100644 --- a/pkg/bot/interactive/plaintext_test.go +++ b/pkg/bot/interactive/plaintext_test.go @@ -55,7 +55,7 @@ func TestInteractiveMessageToPlaintext(t *testing.T) { } // given - help := NewHelpMessage("platform", "testing", nil).Build() + help := NewHelpMessage("platform", "testing", []string{"botkube/kubectl"}).Build() help.ReplaceBotNamePlaceholder("@Botkube") // when diff --git a/pkg/bot/interactive/plugin_help.go b/pkg/bot/interactive/plugin_help.go index 6ccada506..0d0b659a9 100644 --- a/pkg/bot/interactive/plugin_help.go +++ b/pkg/bot/interactive/plugin_help.go @@ -4,20 +4,56 @@ import ( "fmt" "github.com/kubeshop/botkube/pkg/api" + "github.com/kubeshop/botkube/pkg/config" + "github.com/kubeshop/botkube/pkg/formatx" ) -type pluginHelpProviderFn func(platform string, btnBuilder *api.ButtonBuilder) api.Section +type pluginHelpProviderFn func(platform config.CommPlatformIntegration, btnBuilder *api.ButtonBuilder) api.Section var pluginHelpProvider = map[string]pluginHelpProviderFn{ - "botkube/helm": func(platform string, btnBuilder *api.ButtonBuilder) api.Section { + "botkube/helm": func(platform config.CommPlatformIntegration, btnBuilder *api.ButtonBuilder) api.Section { return api.Section{ Base: api.Base{ Header: "Run Helm commands", - Description: fmt.Sprintf("You can run Helm commands directly from %s!", platform), + Description: fmt.Sprintf("You can run Helm commands directly from %s!", platformDisplayName(platform)), }, Buttons: []api.Button{ btnBuilder.ForCommandWithDescCmd("Show help", "helm help"), }, } }, + "botkube/kubectl": func(platform config.CommPlatformIntegration, btnBuilder *api.ButtonBuilder) api.Section { + if platform.IsInteractive() { + return api.Section{ + Base: api.Base{ + Header: "Interactive kubectl - no typing!", + Description: "Build kubectl commands interactively", + }, + Buttons: []api.Button{ + btnBuilder.ForCommandWithDescCmd("kubectl", "kubectl", api.ButtonStylePrimary), + }, + } + } + + // without the kubectl command builder + return api.Section{ + Base: api.Base{ + Header: "Run kubectl commands (if enabled)", + Description: fmt.Sprintf("You can run kubectl commands directly from %s!", platformDisplayName(platform)), + }, + Buttons: []api.Button{ + btnBuilder.ForCommandWithDescCmd(RunCommandName, "kubectl get services"), + btnBuilder.ForCommandWithDescCmd(RunCommandName, "kubectl get pods"), + btnBuilder.ForCommandWithDescCmd(RunCommandName, "kubectl get deployments"), + }, + } + }, +} + +func platformDisplayName(platform config.CommPlatformIntegration) string { + platformName := platform + if platform == config.SocketSlackCommPlatformIntegration { + platformName = "slack" // normalize the SocketSlack to Slack + } + return formatx.ToTitle(platformName) } diff --git a/pkg/bot/interactive/testdata/TestInteractiveMessageToMarkdown/render_with_custom_headers_and_default_new_lines.golden.txt b/pkg/bot/interactive/testdata/TestInteractiveMessageToMarkdown/render_with_custom_headers_and_default_new_lines.golden.txt index dbabd6e4f..58a19ac15 100644 --- a/pkg/bot/interactive/testdata/TestInteractiveMessageToMarkdown/render_with_custom_headers_and_default_new_lines.golden.txt +++ b/pkg/bot/interactive/testdata/TestInteractiveMessageToMarkdown/render_with_custom_headers_and_default_new_lines.golden.txt @@ -28,21 +28,16 @@ By default, Botkube will notify only about cluster errors and recommendations. ``` • `@Botkube show config` +*Manage executors* + • `@Botkube list executors` + • `@Botkube list aliases` + *Run kubectl commands (if enabled)* You can run kubectl commands directly from Platform! • `@Botkube kubectl get services` • `@Botkube kubectl get pods` • `@Botkube kubectl get deployments` -To list all enabled executors - • `@Botkube list executors` - -To list all command aliases - • `@Botkube list aliases` - -*Filters (advanced)* -You can extend Botkube functionality by writing additional filters that can check resource specs, validate some checks and add messages to the Event struct. Learn more at https://docs.botkube.io/filters - *Angry? Amazed?* Give feedback: https://feedback.botkube.io diff --git a/pkg/bot/interactive/testdata/TestInteractiveMessageToMarkdown/render_with_custom_new_lines_and_default_headers.golden.txt b/pkg/bot/interactive/testdata/TestInteractiveMessageToMarkdown/render_with_custom_new_lines_and_default_headers.golden.txt index 48c0ac6f6..534afb117 100644 --- a/pkg/bot/interactive/testdata/TestInteractiveMessageToMarkdown/render_with_custom_new_lines_and_default_headers.golden.txt +++ b/pkg/bot/interactive/testdata/TestInteractiveMessageToMarkdown/render_with_custom_new_lines_and_default_headers.golden.txt @@ -4,4 +4,4 @@ Botkube is now active for "testing" cluster :rocket:

**Ping your cluster* @Botkube [list|enable|disable] action [action name] ```
• `@Botkube list actions`

**View current Botkube configuration**
``` @Botkube show config -```
• `@Botkube show config`

**Run kubectl commands (if enabled)**
You can run kubectl commands directly from Platform!
• `@Botkube kubectl get services`
• `@Botkube kubectl get pods`
• `@Botkube kubectl get deployments`

To list all enabled executors
• `@Botkube list executors`

To list all command aliases
• `@Botkube list aliases`

**Filters (advanced)**
You can extend Botkube functionality by writing additional filters that can check resource specs, validate some checks and add messages to the Event struct. Learn more at https://docs.botkube.io/filters

**Angry? Amazed?**
Give feedback: https://feedback.botkube.io

Read our docs: https://docs.botkube.io
Join our Slack: https://join.botkube.io
Follow us on Twitter: https://twitter.com/botkube_io
\ No newline at end of file +```
• `@Botkube show config`

**Manage executors**
• `@Botkube list executors`
• `@Botkube list aliases`

**Run kubectl commands (if enabled)**
You can run kubectl commands directly from Platform!
• `@Botkube kubectl get services`
• `@Botkube kubectl get pods`
• `@Botkube kubectl get deployments`

**Angry? Amazed?**
Give feedback: https://feedback.botkube.io

Read our docs: https://docs.botkube.io
Join our Slack: https://join.botkube.io
Follow us on Twitter: https://twitter.com/botkube_io
\ No newline at end of file diff --git a/pkg/bot/interactive/testdata/TestInteractiveMessageToPlaintext.golden.txt b/pkg/bot/interactive/testdata/TestInteractiveMessageToPlaintext.golden.txt index 87c4f804c..dfd79ee68 100644 --- a/pkg/bot/interactive/testdata/TestInteractiveMessageToPlaintext.golden.txt +++ b/pkg/bot/interactive/testdata/TestInteractiveMessageToPlaintext.golden.txt @@ -23,21 +23,16 @@ View current Botkube configuration • @Botkube show config +Manage executors + • @Botkube list executors + • @Botkube list aliases + Run kubectl commands (if enabled) You can run kubectl commands directly from Platform! • @Botkube kubectl get services • @Botkube kubectl get pods • @Botkube kubectl get deployments -To list all enabled executors - • @Botkube list executors - -To list all command aliases - • @Botkube list aliases - -Filters (advanced) -You can extend Botkube functionality by writing additional filters that can check resource specs, validate some checks and add messages to the Event struct. Learn more at https://docs.botkube.io/filters - Angry? Amazed? Give feedback: https://feedback.botkube.io diff --git a/pkg/config/config.go b/pkg/config/config.go index cee84708c..7aa282625 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -26,9 +26,6 @@ const ( ) const ( - // AllNamespaceIndicator represents a keyword for allowing all Kubernetes Namespaces. - AllNamespaceIndicator = allValuesPattern - // allValuesPattern represents a keyword for allowing all values. allValuesPattern = ".*" ) @@ -196,9 +193,8 @@ type ActionBindings struct { // Sources contains configuration for Botkube app sources. type Sources struct { - DisplayName string `yaml:"displayName"` - Kubernetes KubernetesSource `yaml:"kubernetes"` - Plugins Plugins `yaml:",inline" koanf:",remain"` + DisplayName string `yaml:"displayName"` + Plugins Plugins `yaml:",inline" koanf:",remain"` } // GetPlugins returns Sources.Plugins. @@ -206,89 +202,6 @@ func (s Sources) GetPlugins() Plugins { return s.Plugins } -// KubernetesSource contains configuration for Kubernetes sources. -type KubernetesSource struct { - Recommendations Recommendations `yaml:"recommendations"` - Event KubernetesEvent `yaml:"event"` - Resources []Resource `yaml:"resources" validate:"dive"` - Namespaces RegexConstraints `yaml:"namespaces"` - Annotations map[string]string `yaml:"annotations"` - Labels map[string]string `yaml:"labels"` -} - -// KubernetesEvent contains configuration for Kubernetes events. -type KubernetesEvent struct { - Reason RegexConstraints `yaml:"reason"` - Message RegexConstraints `yaml:"message"` - Types KubernetesResourceEventTypes `yaml:"types"` -} - -// AreConstraintsDefined checks if any of the event constraints are defined. -func (e KubernetesEvent) AreConstraintsDefined() bool { - return e.Reason.AreConstraintsDefined() || e.Message.AreConstraintsDefined() -} - -// IsAllowed checks if a given resource event is allowed according to the configuration. -func (r *KubernetesSource) IsAllowed(resourceType, namespace string, eventType EventType) bool { - if r == nil || len(r.Resources) == 0 { - return false - } - - isEventAllowed := func(resourceEvents KubernetesResourceEventTypes) bool { - if len(resourceEvents) > 0 { // if resource overrides the global events, use them - return resourceEvents.Contains(eventType) - } - return r.Event.Types.Contains(eventType) // check global events - } - - for _, resource := range r.Resources { - var nsConstraints RegexConstraints - if resource.Namespaces.AreConstraintsDefined() { - nsConstraints = resource.Namespaces - } else { - nsConstraints = r.Namespaces - } - - namespaceAllowed, err := nsConstraints.IsAllowed(namespace) - if err != nil { - // regex error, so don't allow the event - return false - } - - if resource.Type == resourceType && - isEventAllowed(resource.Event.Types) && - namespaceAllowed { - return true - } - } - - return false -} - -// Recommendations contains configuration for various recommendation insights. -type Recommendations struct { - Ingress IngressRecommendations `yaml:"ingress"` - Pod PodRecommendations `yaml:"pod"` -} - -// PodRecommendations contains configuration for pods recommendations. -type PodRecommendations struct { - // NoLatestImageTag notifies about Pod containers that use `latest` tag for images. - NoLatestImageTag *bool `yaml:"noLatestImageTag,omitempty"` - - // LabelsSet notifies about Pod resources created without labels. - LabelsSet *bool `yaml:"labelsSet,omitempty"` -} - -// IngressRecommendations contains configuration for ingress recommendations. -type IngressRecommendations struct { - // BackendServiceValid notifies about Ingress resources with invalid backend service reference. - BackendServiceValid *bool `yaml:"backendServiceValid,omitempty"` - - // TLSSecretValid notifies about Ingress resources with invalid TLS secret reference. - TLSSecretValid *bool `yaml:"tlsSecretValid,omitempty"` -} - // Plugins contains plugins configuration parameters defined in groups. type Plugins map[string]Plugin @@ -302,8 +215,7 @@ type Plugin struct { // PluginContext defines the context for given plugin. type PluginContext struct { // RBAC defines the RBAC rules for given plugin. - RBAC *PolicyRule `yaml:"rbac,omitempty"` - DefaultNamespace string `yaml:"defaultNamespace,omitempty"` + RBAC *PolicyRule `yaml:"rbac,omitempty"` } // PolicyRule is the RBAC rule. @@ -360,17 +272,15 @@ const ( // Executors contains executors configuration parameters. type Executors struct { - Kubectl Kubectl `yaml:"kubectl"` Plugins Plugins `yaml:",inline" koanf:",remain"` } // CollectCommandPrefixes returns list of command prefixes for all executors, even disabled ones. func (e Executors) CollectCommandPrefixes() []string { - prefixes := []string{kubectlCommandName} + var prefixes []string for pluginName := range e.Plugins { prefixes = append(prefixes, ExecutorNameForKey(pluginName)) } - return prefixes } @@ -393,46 +303,6 @@ type Analytics struct { Disable bool `yaml:"disable"` } -// Resource contains resources to watch -type Resource struct { - Type string `yaml:"type"` - Name RegexConstraints `yaml:"name"` - Namespaces RegexConstraints `yaml:"namespaces"` - Annotations map[string]string `yaml:"annotations"` - Labels map[string]string `yaml:"labels"` - Event KubernetesEvent `yaml:"event"` - UpdateSetting UpdateSetting `yaml:"updateSetting"` -} - -// KubernetesResourceEventTypes contains events to watch for a resource. -type KubernetesResourceEventTypes []EventType - -// Contains checks if event is contained in the events slice. -// If the slice contains AllEvent, then the result is true. -func (e *KubernetesResourceEventTypes) Contains(eventType EventType) bool { - if e == nil { - return false - } - - for _, event := range *e { - if event == AllEvent { - return true - } - - if event == eventType { - return true - } - } - - return false -} - -// UpdateSetting struct defines updateEvent fields specification -type UpdateSetting struct { - Fields []string `yaml:"fields"` - IncludeDiff bool `yaml:"includeDiff"` -} - // RegexConstraints contains a list of allowed and excluded values. type RegexConstraints struct { // Include contains a list of allowed values. @@ -602,21 +472,6 @@ type Webhook struct { Bindings SinkBindings `yaml:"bindings" validate:"required_if=Enabled true"` } -// Kubectl configuration for executing commands inside cluster -type Kubectl struct { - Namespaces RegexConstraints `yaml:"namespaces,omitempty"` - Enabled bool `yaml:"enabled"` - Commands Commands `yaml:"commands,omitempty"` - DefaultNamespace string `yaml:"defaultNamespace,omitempty"` - RestrictAccess *bool `yaml:"restrictAccess,omitempty"` -} - -// Commands allowed in bot -type Commands struct { - Verbs []string `yaml:"verbs"` - Resources []string `yaml:"resources"` -} - // CfgWatcher describes configuration for watching the configuration. type CfgWatcher struct { Enabled bool `yaml:"enabled"` diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 1b6c7bc23..2d36a3620 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -49,41 +49,6 @@ func TestLoadConfigSuccess(t *testing.T) { golden.Assert(t, string(gotData), filepath.Join(t.Name(), "config.golden.yaml")) } -func TestLoadConfigWithPlugins(t *testing.T) { - // given - expSourcePlugin := config.Plugins{ - "botkube/keptn": { - Enabled: true, - Config: map[string]interface{}{ - "field": "value", - }, - }, - } - - expExecutorPlugin := config.Plugins{ - "botkube/echo": { - Enabled: true, - Config: map[string]interface{}{ - "changeResponseToUpperCase": true, - }, - }, - } - - files := config.YAMLFiles{ - readTestdataFile(t, "config-all.yaml"), - } - - // when - gotCfg, _, err := config.LoadWithDefaults(files) - - //then - require.NoError(t, err) - require.NotNil(t, gotCfg) - - assert.Equal(t, expSourcePlugin, gotCfg.Sources["k8s-events"].Plugins) - assert.Equal(t, expExecutorPlugin, gotCfg.Executors["plugin-based"].Plugins) -} - func TestNormalizeConfigEnvName(t *testing.T) { // given tests := []struct { @@ -267,37 +232,6 @@ func TestLoadedConfigValidationErrors(t *testing.T) { } } -func TestLoadedConfigValidationWarnings(t *testing.T) { - // given - tests := []struct { - name string - expWarnMsg string - configs [][]byte - }{ - { - name: "executor specifies all and exact namespace in include property", - expWarnMsg: heredoc.Doc(` - 2 errors occurred: - * Key: 'Config.Sources[k8s-events].Kubernetes.Resources[0].Namespaces.Include' Include contains multiple constraints, but it does already include a regex pattern for all values - * Key: 'Config.Executors[kubectl-read-only].Kubectl.Namespaces.Include' Include contains multiple constraints, but it does already include a regex pattern for all values`), - configs: [][]byte{ - readTestdataFile(t, "executors-include-warning.yaml"), - }, - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // when - cfg, details, err := config.LoadWithDefaults(tc.configs) - - // then - assert.NotNil(t, cfg) - assert.NoError(t, err) - assert.EqualError(t, details.ValidateWarnings, tc.expWarnMsg) - }) - } -} - func TestLoadedConfigEnabledPluginErrors(t *testing.T) { // given tests := []struct { diff --git a/pkg/config/testdata/TestLoadConfigSuccess/_aaa-special-file.yaml b/pkg/config/testdata/TestLoadConfigSuccess/_aaa-special-file.yaml index 18431ee4a..9582f8b19 100644 --- a/pkg/config/testdata/TestLoadConfigSuccess/_aaa-special-file.yaml +++ b/pkg/config/testdata/TestLoadConfigSuccess/_aaa-special-file.yaml @@ -5,8 +5,3 @@ communications: # req 1 elm. 'alias': notification: disabled: true - -filters: - kubernetes: - objectAnnotationChecker: false - nodeEventsChecker: true diff --git a/pkg/config/testdata/TestLoadConfigSuccess/actions.yaml b/pkg/config/testdata/TestLoadConfigSuccess/actions.yaml index c2c7283c2..728f3e6b6 100644 --- a/pkg/config/testdata/TestLoadConfigSuccess/actions.yaml +++ b/pkg/config/testdata/TestLoadConfigSuccess/actions.yaml @@ -7,4 +7,4 @@ actions: sources: - k8s-events executors: - - kubectl-read-only + - k8s-tools diff --git a/pkg/config/testdata/TestLoadConfigSuccess/config-all.yaml b/pkg/config/testdata/TestLoadConfigSuccess/config-all.yaml index f9ee9a63d..96f4a66bd 100644 --- a/pkg/config/testdata/TestLoadConfigSuccess/config-all.yaml +++ b/pkg/config/testdata/TestLoadConfigSuccess/config-all.yaml @@ -7,7 +7,7 @@ communications: # req 1 elm. name: 'SLACK_CHANNEL' bindings: executors: - - kubectl-read-only + - k8s-tools sources: - k8s-events token: 'SLACK_API_TOKEN' @@ -24,7 +24,7 @@ communications: # req 1 elm. sources: - k8s-events executors: - - kubectl-read-only + - k8s-tools notification: type: short botToken: 'SLACK_BOT_TOKEN' @@ -42,7 +42,7 @@ communications: # req 1 elm. disabled: true bindings: executors: - - kubectl-read-only + - k8s-tools sources: - k8s-events notification: @@ -54,7 +54,7 @@ communications: # req 1 elm. appPassword: 'APPLICATION_PASSWORD' bindings: executors: - - kubectl-read-only + - k8s-tools sources: - k8s-events notification: @@ -70,7 +70,7 @@ communications: # req 1 elm. id: 'DISCORD_CHANNEL_ID' bindings: executors: - - kubectl-read-only + - k8s-tools sources: - k8s-events notification: @@ -106,7 +106,7 @@ sources: 'k8s-events': displayName: "Plugins & Builtins" - kubernetes: + botkube/kubernetes: recommendations: pod: noLatestImageTag: false @@ -216,8 +216,3 @@ sources: enabled: true config: field: value - -filters: - kubernetes: - objectAnnotationChecker: true - nodeEventsChecker: false diff --git a/pkg/config/testdata/TestLoadConfigSuccess/config.golden.yaml b/pkg/config/testdata/TestLoadConfigSuccess/config.golden.yaml index 1b9bf5d9d..3d40a8a8d 100644 --- a/pkg/config/testdata/TestLoadConfigSuccess/config.golden.yaml +++ b/pkg/config/testdata/TestLoadConfigSuccess/config.golden.yaml @@ -7,371 +7,35 @@ actions: sources: - k8s-events executors: - - kubectl-read-only + - k8s-tools sources: k8s-events: displayName: Plugins & Builtins - kubernetes: - recommendations: - ingress: - backendServiceValid: true - tlsSecretValid: false - pod: - noLatestImageTag: false - labelsSet: true - event: - reason: - include: - - .* - message: - include: - - ^Error .* - types: - - create - - delete - - error - resources: - - type: v1/pods - name: - include: [] - namespaces: - include: [] - annotations: {} - labels: {} - event: - reason: - include: [] - message: - include: [] - types: [] - updateSetting: - fields: [] - includeDiff: false - - type: v1/services - name: - include: [] - namespaces: - include: [] - annotations: {} - labels: {} - event: - reason: - include: [] - message: - include: [] - types: [] - updateSetting: - fields: [] - includeDiff: false - - type: networking.k8s.io/v1/ingresses - name: - include: [] - namespaces: - include: [] - annotations: {} - labels: {} - event: - reason: - include: [] - message: - include: [] - types: [] - updateSetting: - fields: [] - includeDiff: false - - type: v1/nodes - name: - include: [] - namespaces: - include: [] - annotations: {} - labels: {} - event: - reason: - include: - - NodeNotReady - message: - include: - - status .* - types: [] - updateSetting: - fields: [] - includeDiff: false - - type: v1/namespaces - name: - include: [] - namespaces: - include: [] - annotations: {} - labels: {} - event: - reason: - include: [] - message: - include: [] - types: [] - updateSetting: - fields: [] - includeDiff: false - - type: v1/persistentvolumes - name: - include: [] - namespaces: - include: [] - annotations: {} - labels: {} - event: - reason: - include: [] - message: - include: [] - types: [] - updateSetting: - fields: [] - includeDiff: false - - type: v1/persistentvolumeclaims - name: - include: [] - namespaces: - include: [] - annotations: {} - labels: {} - event: - reason: - include: [] - message: - include: [] - types: [] - updateSetting: - fields: [] - includeDiff: false - - type: v1/configmaps - name: - include: [] - namespaces: - include: - - default - exclude: - - kube-system - annotations: {} - labels: {} - event: - reason: - include: [] - message: - include: [] - types: [] - updateSetting: - fields: [] - includeDiff: false - - type: rbac.authorization.k8s.io/v1/roles - name: - include: [] - namespaces: - include: [] - annotations: {} - labels: {} - event: - reason: - include: [] - message: - include: [] - types: [] - updateSetting: - fields: [] - includeDiff: false - - type: rbac.authorization.k8s.io/v1/rolebindings - name: - include: [] - namespaces: - include: [] - annotations: {} - labels: {} - event: - reason: - include: [] - message: - include: [] - types: [] - updateSetting: - fields: [] - includeDiff: false - - type: rbac.authorization.k8s.io/v1/clusterrolebindings - name: - include: [] - namespaces: - include: [] - annotations: {} - labels: {} - event: - reason: - include: [] - message: - include: [] - types: [] - updateSetting: - fields: [] - includeDiff: false - - type: rbac.authorization.k8s.io/v1/clusterroles - name: - include: [] - namespaces: - include: [] - annotations: {} - labels: {} - event: - reason: - include: [] - message: - include: [] - types: [] - updateSetting: - fields: [] - includeDiff: false - - type: apps/v1/daemonsets - name: - include: [] - namespaces: - include: [] - annotations: {} - labels: {} - event: - reason: - include: [] - message: - include: [] - types: - - create - - update - - delete - - error - updateSetting: - fields: - - spec.template.spec.containers[*].image - - status.numberReady - includeDiff: true - - type: batch/v1/jobs - name: - include: - - my-.* - namespaces: - include: [] - annotations: - my-own-annotation: "true" - labels: - my-own-label: "true" - event: - reason: - include: [] - message: - include: [] - types: - - create - - update - - delete - - error - updateSetting: - fields: - - spec.template.spec.containers[*].image - - status.conditions[*].type - includeDiff: true - - type: apps/v1/deployments - name: - include: [] - namespaces: - include: [] - annotations: {} - labels: {} - event: - reason: - include: [] - message: - include: [] - types: - - create - - update - - delete - - error - updateSetting: - fields: - - spec.template.spec.containers[*].image - - status.availableReplicas - includeDiff: true - - type: apps/v1/statefulsets - name: - include: [] - namespaces: - include: [] - annotations: {} - labels: {} - event: - reason: - include: [] - message: - include: [] - types: - - create - - update - - delete - - error - updateSetting: - fields: - - spec.template.spec.containers[*].image - - status.readyReplicas - includeDiff: true - namespaces: - include: - - .* - annotations: - my-annotation: "true" - labels: - my-label: "true" botkube/keptn: enabled: true config: field: value context: {} -executors: - kubectl-read-only: - kubectl: - namespaces: - include: - - .* - exclude: - - foo - - bar - - test-*-ns - enabled: false - commands: - verbs: - - api-resources - - api-versions - - cluster-info - - describe - - diff - - explain - - get - - logs - - top - - auth - resources: - - deployments - - pods - - namespaces - - daemonsets - - statefulsets - - storageclasses - - nodes - defaultNamespace: default - restrictAccess: false - plugin-based: - kubectl: + botkube/kubernetes: enabled: false + config: null + context: {} +executors: + echo: botkube/echo: enabled: true config: changeResponseToUpperCase: true context: {} + k8s-tools: + botkube/helm: + enabled: true + config: null + context: {} + botkube/kubectl: + enabled: true + config: null + context: {} aliases: {} communications: default-workspace: @@ -386,7 +50,7 @@ communications: sources: - k8s-events executors: - - kubectl-read-only + - k8s-tools token: xoxb-token-from-env socketSlack: enabled: true @@ -399,7 +63,7 @@ communications: sources: - k8s-events executors: - - kubectl-read-only + - k8s-tools botToken: xoxb-token-from-env appToken: xapp-token-from-env mattermost: @@ -417,7 +81,7 @@ communications: sources: - k8s-events executors: - - kubectl-read-only + - k8s-tools discord: enabled: false token: DISCORD_TOKEN @@ -431,7 +95,7 @@ communications: sources: - k8s-events executors: - - kubectl-read-only + - k8s-tools teams: enabled: false appID: APPLICATION_ID @@ -441,7 +105,7 @@ communications: sources: - k8s-events executors: - - kubectl-read-only + - k8s-tools webhook: enabled: false url: WEBHOOK_URL diff --git a/pkg/config/testdata/TestLoadConfigSuccess/executors.yaml b/pkg/config/testdata/TestLoadConfigSuccess/executors.yaml index 4468498a4..32fc3eca6 100644 --- a/pkg/config/testdata/TestLoadConfigSuccess/executors.yaml +++ b/pkg/config/testdata/TestLoadConfigSuccess/executors.yaml @@ -1,25 +1,11 @@ executors: - 'kubectl-read-only': - # Kubectl executor configs - kubectl: - namespaces: - include: [ ".*" ] - exclude: [ "foo", "bar", "test-*-ns" ] - - # Set true to enable kubectl commands execution - enabled: false - # List of allowed commands - commands: - # method which are allowed - verbs: [ "api-resources", "api-versions", "cluster-info", "describe", "diff", "explain", "get", "logs", "top", "auth" ] - # resource configuration which is allowed - resources: [ "deployments", "pods" , "namespaces", "daemonsets", "statefulsets", "storageclasses", "nodes" ] - # set Namespace to execute botkube kubectl commands by default - defaultNamespace: default - # Set true to enable commands execution from configured channel only - restrictAccess: false - 'plugin-based': - botkube/echo: # / is syntax for plugin based executors + 'k8s-tools': + botkube/kubectl: + enabled: true + botkube/helm: + enabled: true + 'echo': + botkube/echo: enabled: true config: changeResponseToUpperCase: true diff --git a/pkg/config/testdata/TestLoadConfigWithPlugins/config-all.yaml b/pkg/config/testdata/TestLoadConfigWithPlugins/config-all.yaml index dbc207600..c2860bab1 100644 --- a/pkg/config/testdata/TestLoadConfigWithPlugins/config-all.yaml +++ b/pkg/config/testdata/TestLoadConfigWithPlugins/config-all.yaml @@ -18,7 +18,7 @@ sources: 'k8s-events': displayName: "Plugins & Builtins" - kubernetes: + botkube/kubernetes: events: - create - delete diff --git a/pkg/config/testdata/TestLoadedConfigValidationErrors/missing-alias-command.yaml b/pkg/config/testdata/TestLoadedConfigValidationErrors/missing-alias-command.yaml index a3c617e50..52ff8c6aa 100644 --- a/pkg/config/testdata/TestLoadedConfigValidationErrors/missing-alias-command.yaml +++ b/pkg/config/testdata/TestLoadedConfigValidationErrors/missing-alias-command.yaml @@ -12,7 +12,7 @@ aliases: executors: 'kubectl-read-only': - kubectl: + botkube/kubectl: enabled: true 'helm': botkube/helm: diff --git a/pkg/config/testdata/TestLoadedConfigValidationWarnings/executors-include-warning.yaml b/pkg/config/testdata/TestLoadedConfigValidationWarnings/executors-include-warning.yaml deleted file mode 100644 index 741564807..000000000 --- a/pkg/config/testdata/TestLoadedConfigValidationWarnings/executors-include-warning.yaml +++ /dev/null @@ -1,25 +0,0 @@ -communications: # req 1 elm. - 'default-group': - slack: - enabled: false - token: 'TOKEN' - -executors: - 'kubectl-read-only': - kubectl: - namespaces: - include: [ ".*", "test" ] - exclude: [ "foo", "bar", "test-*-ns" ] - -sources: - k8s-events: - kubernetes: - resources: - - type: v1/pods - namespaces: - include: [ ".*", "kube-system" ] - event: - types: - - create - - delete - - error diff --git a/pkg/config/validator.go b/pkg/config/validator.go index fe903b497..d886f76ab 100644 --- a/pkg/config/validator.go +++ b/pkg/config/validator.go @@ -23,10 +23,8 @@ const ( invalidPluginDefinitionTag = "invalid_plugin_definition" invalidAliasCommandTag = "invalid_alias_command" invalidPluginRBACTag = "invalid_plugin_rbac" - invalidPluginDefaultNSTag = "invalid_plugin_ns" appTokenPrefix = "xapp-" botTokenPrefix = "xoxb-" - kubectlCommandName = "kubectl" ) var warnsOnlyTags = map[string]struct{}{ @@ -126,7 +124,6 @@ func registerBindingsValidator(validate *validator.Validate, trans ut.Translator conflictingPluginVersionTag: "{0}{1}", invalidPluginDefinitionTag: "{0}{1}", invalidPluginRBACTag: "Binding is referencing plugins of same kind with different RBAC. '{0}' and '{1}' bindings must be identical when used together.", - invalidPluginDefaultNSTag: "Binding is referencing plugins of same kind with different default namespace. '{0}' and '{1}' bindings must be identical when used together.", }) } @@ -359,10 +356,6 @@ func validatePluginRBAC[P pluginProvider](sl validator.StructLevel, pluginConfig if !reflect.DeepEqual(firstRBAC, nextCfg.Context.RBAC) { sl.ReportError(bindings, p1, p1, invalidPluginRBACTag, nextIdx) } - - if p1Cfg.Context.DefaultNamespace != nextCfg.Context.DefaultNamespace { - sl.ReportError(bindings, p1, p1, invalidPluginDefaultNSTag, nextIdx) - } } } } diff --git a/pkg/execute/alias_test.go b/pkg/execute/alias_test.go index 119eb347f..0a34c21fc 100644 --- a/pkg/execute/alias_test.go +++ b/pkg/execute/alias_test.go @@ -111,23 +111,23 @@ func fixAliasCfg() config.Config { return config.Config{ Executors: map[string]config.Executors{ "binding1": { - Kubectl: config.Kubectl{ - Enabled: true, - }, Plugins: config.Plugins{ "gh": config.Plugin{ Enabled: false, }, + "botkube/kubectl": config.Plugin{ + Enabled: true, + }, }, }, "binding2": { - Kubectl: config.Kubectl{ - Enabled: false, - }, Plugins: config.Plugins{ "gh": config.Plugin{ Enabled: true, }, + "botkube/kubectl": config.Plugin{ + Enabled: false, + }, }, }, "plugins": { diff --git a/pkg/execute/default_runner.go b/pkg/execute/default_runner.go deleted file mode 100644 index 2a8812a2b..000000000 --- a/pkg/execute/default_runner.go +++ /dev/null @@ -1,65 +0,0 @@ -package execute - -import ( - "os/exec" - "strings" - - "github.com/kubeshop/botkube/pkg/bot/interactive" -) - -// CommandRunner provides functionality to run arbitrary commands. -type CommandRunner interface { - CommandCombinedOutputRunner - CommandSeparateOutputRunner -} - -// CommandCombinedOutputRunner provides functionality to run arbitrary commands. -type CommandCombinedOutputRunner interface { - RunCombinedOutput(command string, args []string) (string, error) -} - -// CommandSeparateOutputRunner provides functionality to run arbitrary commands. -type CommandSeparateOutputRunner interface { - RunSeparateOutput(command string, args []string) (string, string, error) -} - -// OSCommand provides syntax sugar for working with exec.Command -type OSCommand struct{} - -// RunSeparateOutput runs a given command and returns separately its standard output and standard error. -func (*OSCommand) RunSeparateOutput(command string, args []string) (string, string, error) { - var ( - out strings.Builder - outErr strings.Builder - ) - - // #nosec G204 - cmd := exec.Command(command, args...) - cmd.Stdout = &out - cmd.Stderr = &outErr - err := cmd.Run() - - return out.String(), outErr.String(), err -} - -// RunCombinedOutput runs a given command and returns its combined standard output and standard error. -func (*OSCommand) RunCombinedOutput(command string, args []string) (string, error) { - // #nosec G204 - cmd := exec.Command(command, args...) - out, err := cmd.CombinedOutput() - return string(out), err -} - -type ( - executorFunc func() (interactive.CoreMessage, error) - executorsRunner map[string]executorFunc -) - -func (cmds executorsRunner) SelectAndRun(cmdVerb string) (interactive.CoreMessage, error) { - cmdVerb = strings.ToLower(cmdVerb) - fn, found := cmds[cmdVerb] - if !found { - return interactive.CoreMessage{}, errUnsupportedCommand - } - return fn() -} diff --git a/pkg/execute/exec.go b/pkg/execute/exec.go index 7d5ffefbd..c89cdd8ec 100644 --- a/pkg/execute/exec.go +++ b/pkg/execute/exec.go @@ -23,8 +23,6 @@ var ( } ) -const kubectlBuiltinExecutorName = "kubectl" - // ExecExecutor executes all commands that are related to executors. type ExecExecutor struct { log logrus.FieldLogger @@ -84,13 +82,13 @@ func executorsForBindings(executors map[string]config.Executors, bindings []stri } for name, plugin := range executor.Plugins { + enabled := out[name] + if enabled { + // it should stay marked as enabled if at least one is enabled + continue + } out[name] = plugin.Enabled } - - // TODO: Remove once kubectl is migrated to a separate plugin - if executor.Kubectl.Enabled && !out[kubectlBuiltinExecutorName] { - out[kubectlBuiltinExecutorName] = true - } } return out diff --git a/pkg/execute/exec_test.go b/pkg/execute/exec_test.go index b45dca9a9..df0339931 100644 --- a/pkg/execute/exec_test.go +++ b/pkg/execute/exec_test.go @@ -25,14 +25,13 @@ func TestExecutorBindingsExecutor(t *testing.T) { cfg: config.Config{ Executors: map[string]config.Executors{ "kubectl-team-a": { - Kubectl: config.Kubectl{ - Enabled: true, + Plugins: map[string]config.Plugin{ + "botkube/kubectl": { + Enabled: true, + }, }, }, "kubectl-team-b": { - Kubectl: config.Kubectl{ - Enabled: false, - }, Plugins: map[string]config.Plugin{ "botkube/echo": { Enabled: true, @@ -51,28 +50,19 @@ func TestExecutorBindingsExecutor(t *testing.T) { }, bindings: []string{"kubectl-team-a", "kubectl-team-b"}, expOutput: heredoc.Doc(` - EXECUTOR ENABLED ALIASES - botkube/echo true - kubectl true k, kc`), + EXECUTOR ENABLED ALIASES + botkube/echo true + botkube/kubectl true k, kc`), }, { name: "executors and plugins", cfg: config.Config{ Executors: map[string]config.Executors{ - "kubectl-exec-cmd": { - Kubectl: config.Kubectl{ - Enabled: false, - }, - }, - "kubectl-read-only": { - Kubectl: config.Kubectl{ - Enabled: true, - }, - }, - - "kubectl-wait-cmd": { - Kubectl: config.Kubectl{ - Enabled: true, + "kubectl": { + Plugins: config.Plugins{ + "botkube/kubectl": config.Plugin{ + Enabled: true, + }, }, }, "botkube/helm": { @@ -99,12 +89,12 @@ func TestExecutorBindingsExecutor(t *testing.T) { }, }, }, - bindings: []string{"kubectl-exec-cmd", "kubectl-read-only", "kubectl-wait-cmd", "botkube/helm", "botkube/echo@v1.0.1-devel"}, + bindings: []string{"kubectl", "botkube/helm", "botkube/echo@v1.0.1-devel"}, expOutput: heredoc.Doc(` EXECUTOR ENABLED ALIASES botkube/echo@v1.0.1-devel true e botkube/helm true h - kubectl true`), + botkube/kubectl true`), }, } for _, tc := range testCases { diff --git a/pkg/execute/executor.go b/pkg/execute/executor.go index da628c67f..5464e9ce3 100644 --- a/pkg/execute/executor.go +++ b/pkg/execute/executor.go @@ -17,7 +17,6 @@ import ( "github.com/kubeshop/botkube/pkg/config" "github.com/kubeshop/botkube/pkg/execute/alias" "github.com/kubeshop/botkube/pkg/execute/command" - "github.com/kubeshop/botkube/pkg/execute/kubectl" "github.com/kubeshop/botkube/pkg/formatx" ) @@ -38,7 +37,6 @@ type DefaultExecutor struct { cfg config.Config log logrus.FieldLogger analyticsReporter AnalyticsReporter - kubectlExecutor *Kubectl pluginExecutor *PluginExecutor sourceBindingExecutor *SourceBindingExecutor actionExecutor *ActionExecutor @@ -54,29 +52,12 @@ type DefaultExecutor struct { message string platform config.CommPlatformIntegration conversation Conversation - merger *kubectl.Merger commGroupName string user UserInput - kubectlCmdBuilder *KubectlCmdBuilder cmdsMapping *CommandMapping auditReporter audit.AuditReporter } -// CommandFlags creates custom type for flags in botkube -type CommandFlags string - -// Defines botkube flags -const ( - FollowFlag CommandFlags = "--follow" - AbbrFollowFlag CommandFlags = "-f" - WatchFlag CommandFlags = "--watch" - AbbrWatchFlag CommandFlags = "-w" -) - -func (flag CommandFlags) String() string { - return string(flag) -} - // Execute executes commands and returns output func (e *DefaultExecutor) Execute(ctx context.Context) interactive.CoreMessage { empty := interactive.CoreMessage{} @@ -135,40 +116,12 @@ func (e *DefaultExecutor) Execute(ctx context.Context) interactive.CoreMessage { return empty // user specified different target cluster } - // checking if registered plugin overrides the built-in kubectl or kubectl command builder - isPluginCmd := e.pluginExecutor.CanHandle(e.conversation.ExecutorBindings, cmdCtx.Args) - - if e.kubectlExecutor.CanHandle(cmdCtx.Args) && !isPluginCmd { - e.reportCommand(ctx, "kubectl", e.kubectlExecutor.GetCommandPrefix(cmdCtx.Args), cmdCtx.ExecutorFilter.IsActive(), cmdCtx) - out, err := e.kubectlExecutor.Execute(e.conversation.ExecutorBindings, cmdCtx.CleanCmd, e.conversation.IsAuthenticated, cmdCtx) - switch { - case err == nil: - case IsExecutionCommandError(err): - return respond(err.Error(), cmdCtx) - default: - // TODO: Return error when the DefaultExecutor is refactored as a part of https://github.com/kubeshop/botkube/issues/589 - e.log.Errorf("while executing kubectl: %s", err.Error()) - return empty - } - return respond(out, cmdCtx) - } - // commands below are executed only if the channel is authorized if !e.conversation.IsAuthenticated { return empty } - if e.kubectlCmdBuilder.CanHandle(cmdCtx.Args) && !isPluginCmd { - e.reportCommand(ctx, "kubectl-builder", e.kubectlCmdBuilder.GetCommandPrefix(cmdCtx.Args), false, cmdCtx) - out, err := e.kubectlCmdBuilder.Do(ctx, cmdCtx.Args, e.platform, e.conversation.ExecutorBindings, e.conversation.SlackState, header(cmdCtx), cmdCtx) - if err != nil { - // TODO: Return error when the DefaultExecutor is refactored as a part of https://github.com/kubeshop/botkube/issues/589 - e.log.Errorf("while executing kubectl: %s", err.Error()) - return empty - } - return out - } - + isPluginCmd := e.pluginExecutor.CanHandle(e.conversation.ExecutorBindings, cmdCtx.Args) if isPluginCmd { _, fullPluginName := e.pluginExecutor.getEnabledPlugins(e.conversation.ExecutorBindings, cmdCtx.Args[0]) e.reportCommand(ctx, fullPluginName, e.pluginExecutor.GetCommandPrefix(cmdCtx.Args), cmdCtx.ExecutorFilter.IsActive(), cmdCtx) @@ -190,6 +143,11 @@ func (e *DefaultExecutor) Execute(ctx context.Context) interactive.CoreMessage { return out } + help, found := GetInstallHelpForKnownPlugin(cmdCtx.Args) + if found { + return respond(help, cmdCtx) + } + cmdVerb := command.Verb(strings.ToLower(cmdCtx.Args[0])) var cmdRes string if len(cmdCtx.Args) > 1 { diff --git a/pkg/execute/factory.go b/pkg/execute/factory.go index e291f80ee..daa150740 100644 --- a/pkg/execute/factory.go +++ b/pkg/execute/factory.go @@ -13,7 +13,6 @@ import ( "github.com/kubeshop/botkube/pkg/bot/interactive" "github.com/kubeshop/botkube/pkg/config" "github.com/kubeshop/botkube/pkg/execute/command" - "github.com/kubeshop/botkube/pkg/execute/kubectl" ) // DefaultExecutorFactory facilitates creation of the Executor instances. @@ -22,7 +21,6 @@ type DefaultExecutorFactory struct { cfg config.Config analyticsReporter AnalyticsReporter notifierExecutor *NotifierExecutor - kubectlExecutor *Kubectl pluginExecutor *PluginExecutor sourceBindingExecutor *SourceBindingExecutor actionExecutor *ActionExecutor @@ -33,8 +31,6 @@ type DefaultExecutorFactory struct { configExecutor *ConfigExecutor execExecutor *ExecExecutor sourceExecutor *SourceExecutor - merger *kubectl.Merger - kubectlCmdBuilder *KubectlCmdBuilder cmdsMapping *CommandMapping auditReporter audit.AuditReporter } @@ -42,13 +38,9 @@ type DefaultExecutorFactory struct { // DefaultExecutorFactoryParams contains input parameters for DefaultExecutorFactory. type DefaultExecutorFactoryParams struct { Log logrus.FieldLogger - CmdRunner CommandRunner Cfg config.Config - KcChecker *kubectl.Checker - Merger *kubectl.Merger CfgManager config.PersistenceManager AnalyticsReporter AnalyticsReporter - NamespaceLister NamespaceLister CommandGuard CommandGuard PluginManager *plugin.Manager RestCfg *rest.Config @@ -76,13 +68,6 @@ type CommandGuard interface { // NewExecutorFactory creates new DefaultExecutorFactory. func NewExecutorFactory(params DefaultExecutorFactoryParams) (*DefaultExecutorFactory, error) { - kcExecutor := NewKubectl( - params.Log.WithField("component", "Kubectl Executor"), - params.Cfg, - params.Merger, - params.KcChecker, - params.CmdRunner, - ) actionExecutor := NewActionExecutor( params.Log.WithField("component", "Action Executor"), params.CfgManager, @@ -151,13 +136,6 @@ func NewExecutorFactory(params DefaultExecutorFactoryParams) (*DefaultExecutorFa cfg: params.Cfg, analyticsReporter: params.AnalyticsReporter, notifierExecutor: notifierExecutor, - kubectlCmdBuilder: NewKubectlCmdBuilder( - params.Log.WithField("component", "Kubectl Command Builder"), - params.Merger, - kcExecutor, - params.NamespaceLister, - params.CommandGuard, - ), pluginExecutor: NewPluginExecutor( params.Log.WithField("component", "Botkube Plugin Executor"), params.Cfg, @@ -173,8 +151,6 @@ func NewExecutorFactory(params DefaultExecutorFactoryParams) (*DefaultExecutorFa configExecutor: configExecutor, execExecutor: execExecutor, sourceExecutor: sourceExecutor, - merger: params.Merger, - kubectlExecutor: kcExecutor, cmdsMapping: mappings, auditReporter: params.AuditReporter, }, nil @@ -214,7 +190,6 @@ func (f *DefaultExecutorFactory) NewDefault(cfg NewDefaultInput) Executor { log: f.log, cfg: f.cfg, analyticsReporter: f.analyticsReporter, - kubectlExecutor: f.kubectlExecutor, pluginExecutor: f.pluginExecutor, notifierExecutor: f.notifierExecutor, sourceBindingExecutor: f.sourceBindingExecutor, @@ -226,8 +201,6 @@ func (f *DefaultExecutorFactory) NewDefault(cfg NewDefaultInput) Executor { configExecutor: f.configExecutor, execExecutor: f.execExecutor, sourceExecutor: f.sourceExecutor, - merger: f.merger, - kubectlCmdBuilder: f.kubectlCmdBuilder, cmdsMapping: f.cmdsMapping, auditReporter: f.auditReporter, user: cfg.User, diff --git a/pkg/execute/kubectl.go b/pkg/execute/kubectl.go deleted file mode 100644 index 896d402b5..000000000 --- a/pkg/execute/kubectl.go +++ /dev/null @@ -1,294 +0,0 @@ -package execute - -import ( - "fmt" - "strings" - "unicode" - - "github.com/gookit/color" - "github.com/mattn/go-shellwords" - "github.com/sirupsen/logrus" - "github.com/spf13/pflag" - - "github.com/kubeshop/botkube/pkg/config" - "github.com/kubeshop/botkube/pkg/execute/kubectl" - "github.com/kubeshop/botkube/pkg/sliceutil" -) - -const ( - // KubectlBinary is absolute path of kubectl binary - KubectlBinary = "/usr/local/bin/kubectl" -) - -const ( - kubectlNotAuthorizedMsgFmt = "Sorry, this channel is not authorized to execute kubectl command on cluster '%s'." - kubectlNotAllowedVerbMsgFmt = "Sorry, the kubectl '%s' command cannot be executed in the '%s' Namespace on cluster '%s'. Use 'list executors' to see allowed executors." - kubectlNotAllowedVerbInAllNsMsgFmt = "Sorry, the kubectl '%s' command cannot be executed for all Namespaces on cluster '%s'. Use 'list executors' to see allowed executors." - kubectlNotAllowedKindMsgFmt = "Sorry, the kubectl command is not authorized to work with '%s' resources in the '%s' Namespace on cluster '%s'. Use 'list executors' to see allowed executors." - kubectlNotAllowedKinInAllNsMsgFmt = "Sorry, the kubectl command is not authorized to work with '%s' resources for all Namespaces on cluster '%s'. Use 'list executors' to see allowed executors." - kubectlFlagAfterVerbMsg = "Please specify the resource name after the verb, and all flags after the resource name. Format [flags]" - kubectlDefaultNamespace = "default" -) - -// resourcelessCommands holds all commands that don't specify resources directly. For example: -// - kubectl logs foo -// - kubectl cluster-info -var resourcelessCommands = map[string]struct{}{ - "exec": {}, - "logs": {}, - "attach": {}, - "auth": {}, - "api-versions": {}, - "cluster-info": {}, - "cordon": {}, - "drain": {}, - "uncordon": {}, - "run": {}, - "api-resources": {}, -} - -// Kubectl executes kubectl commands using local binary. -type Kubectl struct { - log logrus.FieldLogger - cfg config.Config - - kcChecker *kubectl.Checker - cmdRunner CommandCombinedOutputRunner - merger *kubectl.Merger -} - -// NewKubectl creates a new instance of Kubectl. -func NewKubectl(log logrus.FieldLogger, cfg config.Config, merger *kubectl.Merger, kcChecker *kubectl.Checker, fn CommandCombinedOutputRunner) *Kubectl { - return &Kubectl{ - log: log, - cfg: cfg, - merger: merger, - kcChecker: kcChecker, - cmdRunner: fn, - } -} - -// CanHandle returns true if it's allowed kubectl command that can be handled by this executor. -func (e *Kubectl) CanHandle(args []string) bool { - if len(args) == 0 { - return false - } - - // make sure that verb is also specified - // empty `k|kc|kubectl` commands are handled by command builder - return len(args) >= 2 && args[0] == kubectlCommandName -} - -// GetCommandPrefix gets verb command with k8s alias prefix. -func (e *Kubectl) GetCommandPrefix(args []string) string { - if len(args) < 2 { - return "" - } - - return fmt.Sprintf("%s %s", args[0], args[1]) -} - -// getArgsWithoutAlias gets command without k8s alias. -func (e *Kubectl) getArgsWithoutAlias(msg string) ([]string, error) { - msgParts, err := shellwords.Parse(strings.TrimSpace(msg)) - if err != nil { - return nil, fmt.Errorf("while parsing the command message into args: %w", err) - } - - if len(msgParts) >= 2 && msgParts[0] == kubectlCommandName { - return msgParts[1:], nil - } - - return msgParts, nil -} - -// Execute executes kubectl command based on a given args. -// -// This method should be called ONLY if: -// - we are a target cluster, -// - and Kubectl.CanHandle returned true. -func (e *Kubectl) Execute(bindings []string, command string, isAuthChannel bool, cmdCtx CommandContext) (string, error) { - log := e.log.WithFields(logrus.Fields{ - "isAuthChannel": isAuthChannel, - "command": command, - }) - - log.Debugf("Handling command...") - - args, err := e.getArgsWithoutAlias(command) - if err != nil { - return "", err - } - - var ( - clusterName = e.cfg.Settings.ClusterName - verb = args[0] - resource = e.getResourceName(args) - ) - - executionNs, err := e.getCommandNamespace(args) - if err != nil { - return "", fmt.Errorf("while extracting Namespace from command: %w", err) - } - if executionNs == "" { // namespace not found in command, so find default and add `-n` flag to args - executionNs = e.findDefaultNamespace(bindings) - args = e.addNamespaceFlag(args, executionNs) - } - - kcConfig := e.merger.MergeForNamespace(bindings, executionNs) - - if !isAuthChannel && kcConfig.RestrictAccess { - msg := NewExecutionCommandError(kubectlNotAuthorizedMsgFmt, clusterName) - return "", e.omitIfWeAreNotExplicitlyTargetCluster(log, msg, cmdCtx) - } - - if !e.kcChecker.IsVerbAllowedInNs(kcConfig, verb) { - if executionNs == config.AllNamespaceIndicator { - return "", NewExecutionCommandError(kubectlNotAllowedVerbInAllNsMsgFmt, verb, clusterName) - } - return "", NewExecutionCommandError(kubectlNotAllowedVerbMsgFmt, verb, executionNs, clusterName) - } - - _, isResourceless := resourcelessCommands[verb] - if !isResourceless && resource != "" { - if !e.validResourceName(resource) { - return "", NewExecutionCommandError(kubectlFlagAfterVerbMsg) - } - // Check if user has access to a given Kubernetes resource - // TODO: instead of using config with allowed verbs and commands we simply should use related SA. - if !e.kcChecker.IsResourceAllowedInNs(kcConfig, resource) { - if executionNs == config.AllNamespaceIndicator { - return "", NewExecutionCommandError(kubectlNotAllowedKinInAllNsMsgFmt, resource, clusterName) - } - return "", NewExecutionCommandError(kubectlNotAllowedKindMsgFmt, resource, executionNs, clusterName) - } - } - - finalArgs := e.getFinalArgs(args) - out, err := e.cmdRunner.RunCombinedOutput(KubectlBinary, finalArgs) - out = color.ClearCode(out) - if err != nil { - return "", NewExecutionCommandError("%s%s", out, err.Error()) - } - - return out, nil -} - -// omitIfWeAreNotExplicitlyTargetCluster returns verboseMsg if there is explicit '--cluster-name' flag that matches this cluster. -// It's useful if we want to be more verbose, but we also don't want to spam if we are not the target one. -func (e *Kubectl) omitIfWeAreNotExplicitlyTargetCluster(log *logrus.Entry, verboseMsg *ExecutionCommandError, cmdCtx CommandContext) error { - if cmdCtx.ProvidedClusterNameEqual() { - return verboseMsg - } - - log.WithField("verboseMsg", verboseMsg).Debugf("Skipping kubectl verbose message...") - return nil -} - -// TODO: This code was moved from: -// -// https://github.com/kubeshop/botkube/blob/0b99ac480c8e7e93ce721b345ffc54d89019a812/pkg/execute/executor.go#L242-L276 -// -// Further refactoring in needed. For example, the cluster flag should be removed by an upper layer -// as it's strictly Botkube related and not executor specific (e.g. kubectl, helm, istio etc.). -func (e *Kubectl) getFinalArgs(args []string) []string { - // Remove unnecessary flags - var finalArgs []string - for _, arg := range args { - if arg == AbbrFollowFlag.String() || strings.HasPrefix(arg, FollowFlag.String()) { - continue - } - if arg == AbbrWatchFlag.String() || strings.HasPrefix(arg, WatchFlag.String()) { - continue - } - finalArgs = append(finalArgs, arg) - } - return finalArgs -} - -// getNamespaceFlag returns the namespace value extracted from a given args. -// If `--namespace/-n` was not found, returns empty string. -func (e *Kubectl) getNamespaceFlag(args []string) (string, error) { - f := pflag.NewFlagSet("extract-ns", pflag.ContinueOnError) - f.BoolP("help", "h", false, "to make sure that parsing is ignoring the --help,-h flags") - - // ignore unknown flags errors, e.g. `--cluster-name` etc. - f.ParseErrorsWhitelist.UnknownFlags = true - - var out string - f.StringVarP(&out, "namespace", "n", "", "Kubernetes Namespace") - if err := f.Parse(args); err != nil { - return "", err - } - return out, nil -} - -// getAllNamespaceFlag returns the namespace value extracted from a given args. -// If `--A, --all-namespaces` was not found, returns empty string. -func (e *Kubectl) getAllNamespaceFlag(args []string) (bool, error) { - f := pflag.NewFlagSet("extract-ns", pflag.ContinueOnError) - f.BoolP("help", "h", false, "to make sure that parsing is ignoring the --help,-h flags") - - // ignore unknown flags errors, e.g. `--cluster-name` etc. - f.ParseErrorsWhitelist.UnknownFlags = true - - var out bool - f.BoolVarP(&out, "all-namespaces", "A", false, "Kubernetes All Namespaces") - if err := f.Parse(args); err != nil { - return false, err - } - return out, nil -} - -func (e *Kubectl) getCommandNamespace(args []string) (string, error) { - // 1. Check for `-A, --all-namespaces` in args. Based on the kubectl manual: - // "Namespace in current context is ignored even if specified with --namespace." - inAllNs, err := e.getAllNamespaceFlag(args) - if err != nil { - return "", err - } - if inAllNs { - return config.AllNamespaceIndicator, nil // TODO: find all namespaces - } - - // 2. Check for `-n/--namespace` in args - executionNs, err := e.getNamespaceFlag(args) - if err != nil { - return "", err - } - if executionNs != "" { - return executionNs, nil - } - - return "", nil -} - -func (e *Kubectl) findDefaultNamespace(bindings []string) string { - // 1. Merge all enabled kubectls, to find the defaultNamespace settings - cfg := e.merger.MergeAllEnabled(bindings) - if cfg.DefaultNamespace != "" { - // 2. Use user defined default - return cfg.DefaultNamespace - } - - // 3. If not found, explicitly use `default` namespace. - return kubectlDefaultNamespace -} - -// addNamespaceFlag add namespace to returned args list. -func (e *Kubectl) addNamespaceFlag(args []string, defaultNamespace string) []string { - return append([]string{"-n", defaultNamespace}, sliceutil.FilterEmptyStrings(args)...) -} - -func (e *Kubectl) getResourceName(args []string) string { - if len(args) < 2 { - return "" - } - resource, _, _ := strings.Cut(args[1], "/") - return resource -} - -func (e *Kubectl) validResourceName(resource string) bool { - // ensures that resource name starts with letter - return unicode.IsLetter(rune(resource[0])) -} diff --git a/pkg/execute/kubectl/checker.go b/pkg/execute/kubectl/checker.go deleted file mode 100644 index 5c7a66f6f..000000000 --- a/pkg/execute/kubectl/checker.go +++ /dev/null @@ -1,45 +0,0 @@ -package kubectl - -// ResourceVariantsFunc returns list of alternative namings for a given resource. -type ResourceVariantsFunc func(resource string) []string - -// Checker provides helper functionality to check whether a given kubectl verb and resource are allowed. -type Checker struct { - resourceVariants ResourceVariantsFunc -} - -// NewChecker returns a new Checker instance. -func NewChecker(resourceVariants ResourceVariantsFunc) *Checker { - return &Checker{resourceVariants: resourceVariants} -} - -// IsResourceAllowedInNs returns true if resource was found in a given config. -func (c *Checker) IsResourceAllowedInNs(config EnabledKubectl, resource string) bool { - if len(config.AllowedKubectlResource) == 0 { - return false - } - - // try a given name - if _, found := config.AllowedKubectlResource[resource]; found { - return true - } - - if c.resourceVariants == nil { - return false - } - - // try other variants - for _, name := range c.resourceVariants(resource) { - if _, found := config.AllowedKubectlResource[name]; found { - return true - } - } - - return false -} - -// IsVerbAllowedInNs returns true if verb was found in a given config. -func (c *Checker) IsVerbAllowedInNs(config EnabledKubectl, verb string) bool { - _, found := config.AllowedKubectlVerb[verb] - return found -} diff --git a/pkg/execute/kubectl/checker_test.go b/pkg/execute/kubectl/checker_test.go deleted file mode 100644 index 224c86e00..000000000 --- a/pkg/execute/kubectl/checker_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package kubectl_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/kubeshop/botkube/pkg/execute/kubectl" -) - -func TestKubectlCheckerIsResourceAllowedInNs(t *testing.T) { - tests := []struct { - name string - - namespace string - bindings []string - resource string - variantFn kubectl.ResourceVariantsFunc - - expIsAllowed bool - }{ - { - name: "Should allow deployments", - namespace: "team-a", - resource: "deployments", - bindings: []string{ - "kubectl-team-a", - "kubectl-global", - }, - - expIsAllowed: true, - }, - { - name: "Should allow deploy variant", - namespace: "team-a", - resource: "deploy", - bindings: []string{ - "kubectl-team-a", - "kubectl-global", - }, - variantFn: func(resource string) []string { - if resource == "deploy" { - return []string{"deployments"} - } - return nil - }, - - expIsAllowed: true, - }, - { - name: "Should not allow pods", - namespace: "team-a", - resource: "pods", - bindings: []string{ - "kubectl-team-a", - "kubectl-global", - }, - variantFn: func(resource string) []string { - if resource == "deploy" { - return []string{"deployments"} - } - return nil - }, - - expIsAllowed: false, - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // given - config := kubectl.NewMerger(fixExecutorsConfig(t)).MergeForNamespace(tc.bindings, tc.namespace) - checker := kubectl.NewChecker(tc.variantFn) - - // when - gotIsAllowed := checker.IsResourceAllowedInNs(config, tc.resource) - - // then - assert.Equal(t, tc.expIsAllowed, gotIsAllowed) - }) - } -} - -func TestKubectlCheckerIsVerbAllowedInNs(t *testing.T) { - tests := []struct { - name string - - namespace string - bindings []string - verb string - - expIsAllowed bool - }{ - { - name: "Should allow get from team-a settings", - namespace: "team-a", - verb: "get", - bindings: []string{ - "kubectl-team-a", - "kubectl-global", - }, - - expIsAllowed: true, - }, - { - name: "Should allow logs taken from global settings", - namespace: "team-a", - verb: "logs", - bindings: []string{ - "kubectl-team-a", - "kubectl-global", - }, - - expIsAllowed: true, - }, - { - name: "Should not allow pods", - namespace: "team-a", - verb: "exec", - bindings: []string{ - "kubectl-team-a", - "kubectl-global", - }, - - expIsAllowed: false, - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // given - config := kubectl.NewMerger(fixExecutorsConfig(t)).MergeForNamespace(tc.bindings, tc.namespace) - checker := kubectl.NewChecker(nil) - - // when - gotIsAllowed := checker.IsVerbAllowedInNs(config, tc.verb) - - // then - assert.Equal(t, tc.expIsAllowed, gotIsAllowed) - }) - } -} diff --git a/pkg/execute/kubectl/commander.go b/pkg/execute/kubectl/commander.go deleted file mode 100644 index b68f085cf..000000000 --- a/pkg/execute/kubectl/commander.go +++ /dev/null @@ -1,115 +0,0 @@ -package kubectl - -import ( - "fmt" - "sort" - "strings" - - "github.com/sirupsen/logrus" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/kubeshop/botkube/internal/command" - "github.com/kubeshop/botkube/pkg/config" - "github.com/kubeshop/botkube/pkg/event" -) - -// Command defines a command that is executed by the app. -type Command struct { - Name string - Cmd string -} - -// Commander is responsible for generating kubectl commands for the given event. -type Commander struct { - log logrus.FieldLogger - merger EnabledKubectlMerger - guard CmdGuard -} - -// unsupportedEventCommandVerbs contains list of verbs that are not supported for the actionable event notifications. -var unsupportedEventCommandVerbs = map[string]struct{}{ - "delete": {}, // See https://github.com/kubeshop/botkube/issues/824 -} - -// EnabledKubectlMerger is responsible for merging enabled kubectl commands for the given namespace. -type EnabledKubectlMerger interface { - MergeForNamespace(includeBindings []string, forNamespace string) EnabledKubectl -} - -// CmdGuard is responsible for guarding kubectl commands. -type CmdGuard interface { - GetServerResourceMap() (map[string]metav1.APIResource, error) - GetResourceDetailsFromMap(selectedVerb, resourceType string, resMap map[string]metav1.APIResource) (command.Resource, error) -} - -// NewCommander creates a new Commander instance. -func NewCommander(log logrus.FieldLogger, merger EnabledKubectlMerger, guard CmdGuard) *Commander { - return &Commander{log: log, merger: merger, guard: guard} -} - -// GetCommandsForEvent returns a list of commands for the given event based on the executor bindings. -func (c *Commander) GetCommandsForEvent(event event.Event, executorBindings []string) ([]Command, error) { - if event.Type == config.DeleteEvent { - c.log.Debug("Skipping commands for the DELETE type of event for %q...", event.Kind) - return nil, nil - } - - enabledKubectls := c.merger.MergeForNamespace(executorBindings, event.Namespace) - - resourceTypeParts := strings.Split(event.Resource, "/") - resourceName := resourceTypeParts[len(resourceTypeParts)-1] - - if _, exists := enabledKubectls.AllowedKubectlResource[resourceName]; !exists { - // resource not allowed - return nil, nil - } - - var allowedVerbs []string - for key := range enabledKubectls.AllowedKubectlVerb { - verb := key - if _, exists := unsupportedEventCommandVerbs[verb]; exists { - c.log.Debug("Skipping unsupported verb %q for event notification %q...", verb, event.Kind) - continue - } - - allowedVerbs = append(allowedVerbs, verb) - } - sort.Strings(allowedVerbs) - - resMap, err := c.guard.GetServerResourceMap() - if err != nil { - return nil, err - } - - var commands []Command - for _, verb := range allowedVerbs { - res, err := c.guard.GetResourceDetailsFromMap(verb, resourceName, resMap) - if err != nil { - if err == command.ErrVerbNotSupported { - c.log.Debugf("Not supported verb %q for resource %q. Skipping...", verb, resourceName) - continue - } - - return nil, fmt.Errorf("while getting resource details: %w", err) - } - - var resourceSubstr string - if res.SlashSeparatedInCommand { - resourceSubstr = fmt.Sprintf("%s/%s", resourceName, event.Name) - } else { - resourceSubstr = fmt.Sprintf("%s %s", resourceName, event.Name) - } - - var namespaceSubstr string - if res.Namespaced { - namespaceSubstr = fmt.Sprintf(" --namespace %s", event.Namespace) - } - - commands = append(commands, Command{ - Name: verb, - Cmd: fmt.Sprintf("%s %s%s", verb, resourceSubstr, namespaceSubstr), - }) - } - - return commands, nil -} diff --git a/pkg/execute/kubectl/commander_test.go b/pkg/execute/kubectl/commander_test.go deleted file mode 100644 index a41c786c8..000000000 --- a/pkg/execute/kubectl/commander_test.go +++ /dev/null @@ -1,226 +0,0 @@ -package kubectl_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/kubeshop/botkube/internal/command" - "github.com/kubeshop/botkube/internal/loggerx" - "github.com/kubeshop/botkube/pkg/config" - "github.com/kubeshop/botkube/pkg/event" - "github.com/kubeshop/botkube/pkg/execute/kubectl" -) - -func TestCommander_GetCommandsForEvent(t *testing.T) { - // given - executorBindings := []string{"foo", "bar"} - testCases := []struct { - Name string - Event event.Event - MergedKubectls kubectl.EnabledKubectl - Guard kubectl.CmdGuard - - ExpectedResult []kubectl.Command - ExpectedErrMessage string - }{ - { - Name: "Skip delete event", - Event: event.Event{ - Resource: "apps/v1/deployments", - Name: "foo", - Namespace: "default", - Type: config.DeleteEvent, - }, - Guard: nil, - MergedKubectls: kubectl.EnabledKubectl{}, - ExpectedResult: nil, - ExpectedErrMessage: "", - }, - { - Name: "Resource not allowed", - Event: event.Event{ - Resource: "apps/v1/deployments", - Name: "foo", - Namespace: "default", - Type: config.CreateEvent, - }, - Guard: nil, - MergedKubectls: kubectl.EnabledKubectl{ - AllowedKubectlResource: map[string]struct{}{ - "services": {}, - "pods": {}, - }, - AllowedKubectlVerb: map[string]struct{}{ - "get": {}, - "describe": {}, - }, - }, - ExpectedResult: nil, - ExpectedErrMessage: "", - }, - { - Name: "Namespaced resource", - Event: event.Event{ - Resource: "v1/pods", - Name: "foo", - Namespace: "default", - Type: config.CreateEvent, - }, - Guard: &fakeGuard{ - resMap: map[string]metav1.APIResource{ - "pods": {Name: "pods", Namespaced: true, Kind: "Pod", Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"}}, - "nodes": {Name: "nodes", Namespaced: false, Kind: "Node", Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"}, ShortNames: []string{"no"}}, - }, - verbMap: fixVerbMapForFakeGuard(), - }, - MergedKubectls: kubectl.EnabledKubectl{ - AllowedKubectlResource: map[string]struct{}{ - "services": {}, - "pods": {}, - "deployments": {}, - }, - AllowedKubectlVerb: map[string]struct{}{ - "get": {}, - "delete": {}, // Ignore not supported event command verbs - "describe": {}, - "logs": {}, - }, - }, - ExpectedResult: []kubectl.Command{ - {Name: "describe", Cmd: "describe pods foo --namespace default"}, - {Name: "get", Cmd: "get pods foo --namespace default"}, - {Name: "logs", Cmd: "logs pods/foo --namespace default"}, - }, - ExpectedErrMessage: "", - }, - { - Name: "Cluster-wide resource", - Event: event.Event{ - Resource: "v1/nodes", - Name: "foo", - Type: config.UpdateEvent, - }, - Guard: &fakeGuard{ - resMap: map[string]metav1.APIResource{ - "pods": {Name: "pods", Namespaced: true, Kind: "Pod", Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"}}, - "nodes": {Name: "nodes", Namespaced: false, Kind: "Node", Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"}, ShortNames: []string{"no"}}, - }, - verbMap: fixVerbMapForFakeGuard(), - }, - MergedKubectls: kubectl.EnabledKubectl{ - AllowedKubectlResource: map[string]struct{}{ - "services": {}, - "pods": {}, - "nodes": {}, - "deployments": {}, - }, - AllowedKubectlVerb: map[string]struct{}{ - "get": {}, - "describe": {}, - "logs": {}, - }, - }, - ExpectedResult: []kubectl.Command{ - {Name: "describe", Cmd: "describe nodes foo"}, - {Name: "get", Cmd: "get nodes foo"}, - }, - ExpectedErrMessage: "", - }, - } - - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - cmder := kubectl.NewCommander(loggerx.NewNoop(), &fakeMerger{res: tc.MergedKubectls}, tc.Guard) - - // when - result, err := cmder.GetCommandsForEvent(tc.Event, executorBindings) - - // then - if tc.ExpectedErrMessage != "" { - require.Error(t, err) - assert.Equal(t, tc.ExpectedErrMessage, err.Error()) - return - } - - require.NoError(t, err) - assert.Equal(t, tc.ExpectedResult, result) - }) - } -} - -type fakeMerger struct { - res kubectl.EnabledKubectl -} - -func (f *fakeMerger) MergeForNamespace(_ []string, _ string) kubectl.EnabledKubectl { - return f.res -} - -type fakeGuard struct { - resMap map[string]metav1.APIResource - verbMap map[string]map[string]command.Resource -} - -func (f *fakeGuard) GetServerResourceMap() (map[string]metav1.APIResource, error) { - return f.resMap, nil -} - -func (f *fakeGuard) GetResourceDetailsFromMap(selectedVerb, resourceType string, _ map[string]metav1.APIResource) (command.Resource, error) { - resources, ok := f.verbMap[selectedVerb] - if !ok { - return command.Resource{}, command.ErrVerbNotSupported - } - - res, ok := resources[resourceType] - if !ok { - return command.Resource{}, command.ErrVerbNotSupported - } - - return res, nil -} - -func fixVerbMapForFakeGuard() map[string]map[string]command.Resource { - return map[string]map[string]command.Resource{ - "get": { - "pods": { - Name: "pods", - SlashSeparatedInCommand: false, - Namespaced: true, - }, - "nodes": { - Name: "nodes", - Namespaced: false, - SlashSeparatedInCommand: false, - }, - }, - "describe": { - "pods": { - Name: "pods", - SlashSeparatedInCommand: false, - Namespaced: true, - }, - "nodes": { - Name: "nodes", - Namespaced: false, - SlashSeparatedInCommand: false, - }, - }, - "logs": { - "pods": { - Name: "pods", - SlashSeparatedInCommand: true, - Namespaced: true, - }, - }, - "delete": { - "pods": { - Name: "pods", - SlashSeparatedInCommand: false, - Namespaced: true, - }, - }, - } -} diff --git a/pkg/execute/kubectl/helpers_test.go b/pkg/execute/kubectl/helpers_test.go deleted file mode 100644 index db7bfebe9..000000000 --- a/pkg/execute/kubectl/helpers_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package kubectl_test - -import ( - "testing" - - "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" - - "github.com/kubeshop/botkube/pkg/config" -) - -var RawExecutorsConfig = ` -executors: - 'kubectl-team-a': - kubectl: - enabled: true - namespaces: - include: [ "team-a" ] - commands: - verbs: [ "get" ] - resources: [ "deployments" ] - defaultNamespace: "team-a" - restrictAccess: false - 'kubectl-team-b': - kubectl: - enabled: true - namespaces: - include: [ "team-b" ] - commands: - verbs: [ "get", "describe" ] - resources: [ "deployments", "pods" ] - 'kubectl-global': - kubectl: - enabled: true - namespaces: - include: [ ".*" ] - commands: - verbs: [ "logs", "top" ] - resources: [ ] - restrictAccess: true - 'kubectl-all': - kubectl: - enabled: true - namespaces: - include: [ ".*" ] - commands: - verbs: [ "cluster-info" ] - resources: [ ] - defaultNamespace: "foo" - 'kubectl-exec': - kubectl: - enabled: false - namespaces: - include: [ ".*" ] - commands: - verbs: [ "exec" ] - resources: [ ]` - -func fixExecutorsConfig(t *testing.T) map[string]config.Executors { - t.Helper() - - var givenCfg config.Config - err := yaml.Unmarshal([]byte(RawExecutorsConfig), &givenCfg) - require.NoError(t, err) - - return givenCfg.Executors -} diff --git a/pkg/execute/kubectl/merger.go b/pkg/execute/kubectl/merger.go deleted file mode 100644 index 846347f15..000000000 --- a/pkg/execute/kubectl/merger.go +++ /dev/null @@ -1,149 +0,0 @@ -package kubectl - -import ( - "github.com/kubeshop/botkube/pkg/config" -) - -// EnabledKubectl configuration for executing commands inside cluster -type EnabledKubectl struct { - AllowedKubectlVerb map[string]struct{} - AllowedKubectlResource map[string]struct{} - - AllowedNamespacesPerResource map[string]config.RegexConstraints - - DefaultNamespace string - RestrictAccess bool -} - -// Merger provides functionality to merge multiple bindings -// associated with the kubectl executor. -type Merger struct { - executors map[string]config.Executors -} - -// NewMerger returns a new Merger instance. -func NewMerger(executors map[string]config.Executors) *Merger { - return &Merger{ - executors: executors, - } -} - -// MergeForNamespace returns kubectl configuration for a given set of bindings. -// -// It merges entries only if a given Namespace is matched. -// - kubectl.commands.verbs - strategy append -// - kubectl.commands.resources - strategy append -// - kubectl.defaultNamespace - strategy override (if not empty) -// - kubectl.restrictAccess - strategy override (if not empty) -// -// The order of merging is the same as the order of items specified in the includeBindings list. -func (kc *Merger) MergeForNamespace(includeBindings []string, forNamespace string) EnabledKubectl { - enabledInNs := func(executor config.Kubectl) bool { - nsAllowed, err := executor.Namespaces.IsAllowed(forNamespace) - if err != nil { - // regex error - return false - } - - return executor.Enabled && nsAllowed - } - return kc.merge(kc.collect(includeBindings, enabledInNs), includeBindings) -} - -// MergeAllEnabled returns kubectl configuration for all kubectl configs. -func (kc *Merger) MergeAllEnabled(includeBindings []string) EnabledKubectl { - return kc.merge(kc.GetAllEnabled(includeBindings), includeBindings) -} - -// GetAllEnabled returns the collection of enabled kubectl executors for a given list of bindings without merging them. -func (kc *Merger) GetAllEnabled(includeBindings []string) map[string]config.Kubectl { - onlyEnabled := func(executor config.Kubectl) bool { - return executor.Enabled - } - return kc.collect(includeBindings, onlyEnabled) -} - -// IsAtLeastOneEnabled returns true if at least one kubectl executor is enabled. -func (kc *Merger) IsAtLeastOneEnabled() bool { - for _, executor := range kc.executors { - if executor.Kubectl.Enabled { - return true - } - } - return false -} - -func (kc *Merger) merge(collectedKubectls map[string]config.Kubectl, mapKeyOrder []string) EnabledKubectl { - if len(collectedKubectls) == 0 { - return EnabledKubectl{} - } - - var ( - defaultNs string - restrictAccess bool - - allowedResources = map[string]struct{}{} - allowedVerbs = map[string]struct{}{} - allowedNSPerResource = map[string]config.RegexConstraints{} - ) - for _, name := range mapKeyOrder { - item, found := collectedKubectls[name] - if !found { - continue - } - - for _, resourceName := range item.Commands.Resources { - allowedResources[resourceName] = struct{}{} - ns, found := allowedNSPerResource[resourceName] - if !found { - allowedNSPerResource[resourceName] = item.Namespaces - } - ns.Exclude = append(ns.Exclude, item.Namespaces.Exclude...) - ns.Include = append(ns.Include, item.Namespaces.Include...) - allowedNSPerResource[resourceName] = ns - } - - for _, verbName := range item.Commands.Verbs { - allowedVerbs[verbName] = struct{}{} - } - - if item.DefaultNamespace != "" { - defaultNs = item.DefaultNamespace - } - - if item.RestrictAccess != nil { - restrictAccess = *item.RestrictAccess - } - } - - return EnabledKubectl{ - AllowedKubectlResource: allowedResources, - AllowedKubectlVerb: allowedVerbs, - AllowedNamespacesPerResource: allowedNSPerResource, - DefaultNamespace: defaultNs, - RestrictAccess: restrictAccess, - } -} - -type collectPredicateFunc func(executor config.Kubectl) bool - -func (kc *Merger) collect(includeBindings []string, predicate collectPredicateFunc) map[string]config.Kubectl { - if kc.executors == nil { - return nil - } - out := map[string]config.Kubectl{} - for _, name := range includeBindings { - executor, found := kc.executors[name] - if !found { - continue - } - - if !predicate(executor.Kubectl) { - continue - } - - out[name] = executor.Kubectl - } - - return out -} diff --git a/pkg/execute/kubectl/merger_test.go b/pkg/execute/kubectl/merger_test.go deleted file mode 100644 index ad6273318..000000000 --- a/pkg/execute/kubectl/merger_test.go +++ /dev/null @@ -1,197 +0,0 @@ -package kubectl_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/kubeshop/botkube/pkg/config" - "github.com/kubeshop/botkube/pkg/execute/kubectl" -) - -func TestKubectlMerger(t *testing.T) { - // given - tests := []struct { - name string - - givenBindings []string - expectKubectlConfig kubectl.EnabledKubectl - givenNamespace string - }{ - { - name: "Should collect settings with ignored settings for team-b", - givenBindings: []string{ - "kubectl-team-a", - "kubectl-team-b", - "kubectl-global", - "kubectl-exec", - }, - givenNamespace: "team-a", - expectKubectlConfig: kubectl.EnabledKubectl{ - AllowedNamespacesPerResource: map[string]config.RegexConstraints{ - "deployments": { - Include: []string{ - "team-a", - }, - }, - }, - AllowedKubectlVerb: map[string]struct{}{ - "get": {}, - "logs": {}, - "top": {}, - }, - AllowedKubectlResource: map[string]struct{}{ - "deployments": {}, - }, - DefaultNamespace: "team-a", - RestrictAccess: true, - }, - }, - { - name: "Should collect settings only for 'all' namespace", - givenBindings: []string{ - "kubectl-team-a", - "kubectl-team-b", - "kubectl-global", - "kubectl-exec", - "kubectl-all", - }, - givenNamespace: config.AllNamespaceIndicator, - expectKubectlConfig: kubectl.EnabledKubectl{ - AllowedNamespacesPerResource: map[string]config.RegexConstraints{}, - AllowedKubectlVerb: map[string]struct{}{ - "logs": {}, - "top": {}, - "cluster-info": {}, - }, - AllowedKubectlResource: map[string]struct{}{}, - DefaultNamespace: "foo", - RestrictAccess: true, - }, - }, - { - name: "Should collect only team-a settings", - givenBindings: []string{ - "kubectl-team-a", - "kubectl-team-b", - }, - givenNamespace: "team-a", - expectKubectlConfig: kubectl.EnabledKubectl{ - AllowedNamespacesPerResource: map[string]config.RegexConstraints{ - "deployments": { - Include: []string{ - "team-a", - }, - }, - }, - AllowedKubectlVerb: map[string]struct{}{ - "get": {}, - }, - AllowedKubectlResource: map[string]struct{}{ - "deployments": {}, - }, - DefaultNamespace: "team-a", - RestrictAccess: false, - }, - }, - { - name: "Should enable restrict access based on the bindings order", - givenBindings: []string{ - "kubectl-team-a", // disables restrict - "kubectl-global", // enables restrict - }, - givenNamespace: "team-a", - expectKubectlConfig: kubectl.EnabledKubectl{ - AllowedNamespacesPerResource: map[string]config.RegexConstraints{ - "deployments": { - Include: []string{ - "team-a", - }, - }, - }, - AllowedKubectlVerb: map[string]struct{}{ - "get": {}, - "logs": {}, - "top": {}, - }, - AllowedKubectlResource: map[string]struct{}{ - "deployments": {}, - }, - DefaultNamespace: "team-a", - RestrictAccess: true, - }, - }, - { - name: "Should disable restrict access based on the bindings order", - givenBindings: []string{ - "kubectl-global", // enables restrict - "kubectl-team-a", // disables restrict - }, - givenNamespace: "team-a", - expectKubectlConfig: kubectl.EnabledKubectl{ - AllowedNamespacesPerResource: map[string]config.RegexConstraints{ - "deployments": { - Include: []string{ - "team-a", - }, - }, - }, - AllowedKubectlVerb: map[string]struct{}{ - "get": {}, - "logs": {}, - "top": {}, - }, - AllowedKubectlResource: map[string]struct{}{ - "deployments": {}, - }, - DefaultNamespace: "team-a", - RestrictAccess: false, - }, - }, - { - name: "Should enable restrict access when it's not specified in other bindings", - givenBindings: []string{ - "kubectl-global", // enables restrict - "kubectl-team-b", // doesn't specify restrict - }, - givenNamespace: "team-b", - expectKubectlConfig: kubectl.EnabledKubectl{ - AllowedNamespacesPerResource: map[string]config.RegexConstraints{ - "deployments": { - Include: []string{ - "team-b", - }, - }, - "pods": { - Include: []string{ - "team-b", - }, - }, - }, - AllowedKubectlVerb: map[string]struct{}{ - "get": {}, - "describe": {}, - "logs": {}, - "top": {}, - }, - AllowedKubectlResource: map[string]struct{}{ - "deployments": {}, - "pods": {}, - }, - RestrictAccess: true, - }, - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // given - kubectlMerger := kubectl.NewMerger(fixExecutorsConfig(t)) - - // when - gotKubectlConfig := kubectlMerger.MergeForNamespace(tc.givenBindings, tc.givenNamespace) - - // then - assert.Equal(t, tc.expectKubectlConfig, gotKubectlConfig) - }) - } -} diff --git a/pkg/execute/kubectl/resource-normalizer.go b/pkg/execute/kubectl/resource-normalizer.go deleted file mode 100644 index 5d1bd380b..000000000 --- a/pkg/execute/kubectl/resource-normalizer.go +++ /dev/null @@ -1,84 +0,0 @@ -package kubectl - -import ( - "fmt" - "strings" - - "github.com/sirupsen/logrus" - "k8s.io/client-go/discovery" -) - -// ResourceNormalizer contains helper maps to normalize the resource name specified in the kubectl command. -type ResourceNormalizer struct { - kindResourceMap map[string]string - shortnameResourceMap map[string]string -} - -// NewResourceNormalizer returns new ResourceNormalizer instance. -func NewResourceNormalizer(log logrus.FieldLogger, discoveryCli discovery.DiscoveryInterface) (ResourceNormalizer, error) { - resMapping := ResourceNormalizer{ - kindResourceMap: make(map[string]string), - shortnameResourceMap: make(map[string]string), - } - - _, resourceList, err := discoveryCli.ServerGroupsAndResources() - if err != nil { - if !shouldIgnoreResourceListError(err) { - return ResourceNormalizer{}, fmt.Errorf("while getting resource list from K8s cluster: %w", err) - } - - log.Warnf("Ignoring error while getting resource list from K8s cluster: %s", err.Error()) - } - - for _, resource := range resourceList { - for _, r := range resource.APIResources { - // Exclude subresources - if strings.Contains(r.Name, "/") { - continue - } - resMapping.kindResourceMap[strings.ToLower(r.Kind)] = r.Name - for _, sn := range r.ShortNames { - resMapping.shortnameResourceMap[sn] = r.Name - } - } - } - log.Debugf("Loaded resource mapping: %+v", resMapping) - return resMapping, nil -} - -// Normalize returns list with alternative names for a given input resource. -func (r ResourceNormalizer) Normalize(in string) []string { - variants := []string{ - // normalized received name - strings.ToLower(in), - // normalized short name - r.shortnameResourceMap[strings.ToLower(in)], - // normalized kind name - r.kindResourceMap[strings.ToLower(in)], - } - return variants -} - -// shouldIgnoreResourceListError returns true if the error should be ignored. This is a workaround for client-go behavior, -// which reports error on empty resource lists. However, some components can register empty lists for their resources. -// See -// See: https://github.com/kyverno/kyverno/issues/2267 -func shouldIgnoreResourceListError(err error) bool { - groupDiscoFailedErr, ok := err.(*discovery.ErrGroupDiscoveryFailed) - if !ok { - return false - } - - for _, currentErr := range groupDiscoFailedErr.Groups { - // Unfortunately there isn't a nicer way to do this. - // See https://github.com/kubernetes/client-go/blob/release-1.25/discovery/cached/memory/memcache.go#L228 - if strings.Contains(currentErr.Error(), "Got empty response for") { - // ignore it as it isn't necessarily an error - continue - } - - return false - } - - return true -} diff --git a/pkg/execute/kubectl_cmd_builder.go b/pkg/execute/kubectl_cmd_builder.go deleted file mode 100644 index 888221422..000000000 --- a/pkg/execute/kubectl_cmd_builder.go +++ /dev/null @@ -1,519 +0,0 @@ -package execute - -import ( - "context" - "errors" - "fmt" - "strings" - - "github.com/google/uuid" - "github.com/sirupsen/logrus" - "github.com/slack-go/slack" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/kubeshop/botkube/pkg/api" - "github.com/kubeshop/botkube/pkg/bot/interactive" - "github.com/kubeshop/botkube/pkg/config" - "github.com/kubeshop/botkube/pkg/execute/kubectl" - "github.com/kubeshop/botkube/pkg/maputil" -) - -const ( - verbsDropdownCommand = "kc-cmd-builder --verbs" - resourceTypesDropdownCommand = "kc-cmd-builder --resource-type" - resourceNamesDropdownCommand = "kc-cmd-builder --resource-name" - resourceNamespaceDropdownCommand = "kc-cmd-builder --namespace" - filterPlaintextInputCommand = "kc-cmd-builder --filter-query" - kubectlCommandName = "kubectl" - dropdownItemsLimit = 100 - noKubectlCommandsInChannel = "No `kubectl` commands are enabled in this channel. To learn how to enable them, visit https://docs.botkube.io/configuration/executor." - kubectlMissingCommandMsg = "Please specify the kubectl command" -) - -var knownCmdPrefix = map[string]struct{}{ - verbsDropdownCommand: {}, - resourceTypesDropdownCommand: {}, - resourceNamesDropdownCommand: {}, - resourceNamespaceDropdownCommand: {}, - filterPlaintextInputCommand: {}, -} - -var errRequiredVerbDropdown = errors.New("verbs dropdown select cannot be empty") - -type ( - kcMerger interface { - MergeAllEnabled(includeBindings []string) kubectl.EnabledKubectl - } - kcExecutor interface { - Execute(bindings []string, command string, isAuthChannel bool, cmdCtx CommandContext) (string, error) - } - // NamespaceLister provides an option to list all namespaces in a given cluster. - NamespaceLister interface { - List(ctx context.Context, opts metav1.ListOptions) (*corev1.NamespaceList, error) - } -) - -// KubectlCmdBuilder provides functionality to handle interactive kubectl command selection. -type KubectlCmdBuilder struct { - log logrus.FieldLogger - kcExecutor kcExecutor - merger kcMerger - namespaceLister NamespaceLister - commandGuard CommandGuard -} - -// NewKubectlCmdBuilder returns a new KubectlCmdBuilder instance. -func NewKubectlCmdBuilder(log logrus.FieldLogger, merger kcMerger, executor kcExecutor, namespaceLister NamespaceLister, guard CommandGuard) *KubectlCmdBuilder { - return &KubectlCmdBuilder{ - log: log, - kcExecutor: executor, - merger: merger, - namespaceLister: namespaceLister, - commandGuard: guard, - } -} - -// CanHandle returns true if it's allowed kubectl command that can be handled by this executor. -func (e *KubectlCmdBuilder) CanHandle(args []string) bool { - // if we know the command prefix, we can handle that command :) - return e.GetCommandPrefix(args) != "" -} - -// GetCommandPrefix returns the command prefix only if it's known. -func (e *KubectlCmdBuilder) GetCommandPrefix(args []string) string { - switch len(args) { - case 0: - return "" - - case 1: - // check if it's only a kubectl command without arguments - if args[0] == kubectlCommandName { - return args[0] - } - // it is a single arg which cannot start the kubectl command builder - return "" - - default: - // check if request comes from command builder message - gotCmd := fmt.Sprintf("%s %s", args[0], args[1]) - if _, found := knownCmdPrefix[gotCmd]; found { - return gotCmd - } - return "" - } -} - -// Do executes a given kc-cmd-builder command based on args. -// -// TODO: once we will have a real use-case, we should abstract the Slack state and introduce our own model. -func (e *KubectlCmdBuilder) Do(ctx context.Context, args []string, platform config.CommPlatformIntegration, bindings []string, state *slack.BlockActionStates, header string, cmdCtx CommandContext) (interactive.CoreMessage, error) { - var empty interactive.CoreMessage - - if !platform.IsInteractive() { - e.log.Debug("Interactive kubectl command builder is not supported on %s platform", platform) - return e.message(header, kubectlMissingCommandMsg) - } - - allVerbs, allTypes, defaultNs := e.getEnableKubectlDetails(bindings) - if len(allVerbs) == 0 { - return e.message(header, noKubectlCommandsInChannel) - } - - allVerbs = e.commandGuard.FilterSupportedVerbs(allVerbs) - - // if only command name was specified, return initial command builder message - if len(args) == 1 { - return e.initialMessage(allVerbs) - } - - stateDetails := e.extractStateDetails(state) - if stateDetails.namespace == "" { - stateDetails.namespace = defaultNs - } - - var ( - cmdName = args[0] - cmdVerb = args[1] - cmd = fmt.Sprintf("%s %s", cmdName, cmdVerb) - ) - - cmds := executorsRunner{ - verbsDropdownCommand: func() (interactive.CoreMessage, error) { - return e.renderMessage(ctx, stateDetails, bindings, allVerbs, allTypes, cmdCtx) - }, - resourceTypesDropdownCommand: func() (interactive.CoreMessage, error) { - // the resource type was selected, so clear resource name from command preview. - stateDetails.resourceName = "" - return e.renderMessage(ctx, stateDetails, bindings, allVerbs, allTypes, cmdCtx) - }, - resourceNamesDropdownCommand: func() (interactive.CoreMessage, error) { - // this is called only when the resource name is directly selected from dropdown, so we need to include - // it in command preview. - return e.renderMessage(ctx, stateDetails, bindings, allVerbs, allTypes, cmdCtx) - }, - resourceNamespaceDropdownCommand: func() (interactive.CoreMessage, error) { - // when the namespace was changed, there is a small chance that resource name will be still matching, - // we will need to do the external call to check that. For now, we clear resource name from command preview. - stateDetails.resourceName = "" - return e.renderMessage(ctx, stateDetails, bindings, allVerbs, allTypes, cmdCtx) - }, - filterPlaintextInputCommand: func() (interactive.CoreMessage, error) { - return e.renderMessage(ctx, stateDetails, bindings, allVerbs, allTypes, cmdCtx) - }, - } - - msg, err := cmds.SelectAndRun(cmd) - if err != nil { - e.log.WithField("error", err.Error()).Error("Cannot render the kubectl command builder. Returning empty message.") - return empty, err - } - return msg, nil -} - -func (e *KubectlCmdBuilder) initialMessage(allVerbs []string) (interactive.CoreMessage, error) { - var empty interactive.CoreMessage - - // We start a new interactive block, so we generate unique ID. - // Later when we update this message with a new "body" e.g. update command preview - // the block state remains the same as Slack always see it under the same id. - // If we use different ID each time we update the message, Slack will clean up the state - // meaning we will lose information about verb/resourceType/resourceName that were previously selected. - id, err := uuid.NewRandom() - if err != nil { - return empty, err - } - allVerbsSelect := VerbSelect(allVerbs, "") - if allVerbsSelect == nil { - return empty, errRequiredVerbDropdown - } - - msg := KubectlCmdBuilderMessage(id.String(), *allVerbsSelect) - // we are the initial message, don't replace the original one as we need to send a brand-new message visible only to the user - // otherwise we can replace a message that is publicly visible. - msg.ReplaceOriginal = false - - return msg, nil -} - -func (e *KubectlCmdBuilder) renderMessage(ctx context.Context, stateDetails stateDetails, bindings, allVerbs, allTypes []string, cmdCtx CommandContext) (interactive.CoreMessage, error) { - var empty interactive.CoreMessage - - allVerbsSelect := VerbSelect(allVerbs, stateDetails.verb) - if allVerbsSelect == nil { - return empty, errRequiredVerbDropdown - } - - // 1. Refresh resource type list - matchingTypes, err := e.getAllowedResourcesSelectList(stateDetails.verb, allTypes, stateDetails.resourceType) - if err != nil { - return empty, err - } - - // 2. If a given verb doesn't have assigned resource types, - // render: - // 1. Dropdown with all verbs - // 2. Filter input - // 3. Command preview. For example: - // kubectl api-resources - if matchingTypes == nil { - // we must zero those fields as they are known only if we know the resource type and this verb doesn't have one :) - stateDetails.resourceType = "" - stateDetails.resourceName = "" - stateDetails.namespace = "" - preview := e.buildCommandPreview(stateDetails) - - return KubectlCmdBuilderMessage( - stateDetails.dropdownsBlockID, *allVerbsSelect, - WithAdditionalSections(preview...), - ), nil - } - - // 3. If resource type is not on the listy anymore, - // render: - // 1. Dropdown with all verbs - // 2. Dropdown with all related resource types - // because we don't know the resource type we cannot render: - // 1. Resource names - obvious :). - // 2. Namespaces as we don't know if it's cluster or namespace scoped resource. - if !e.contains(matchingTypes, stateDetails.resourceType) { - return KubectlCmdBuilderMessage( - stateDetails.dropdownsBlockID, *allVerbsSelect, - WithAdditionalSelects(matchingTypes), - ), nil - } - - // At this stage we know that: - // 1. Verb requires resource types - // 2. Selected resource type is still valid for the selected verb - var ( - resNames = e.tryToGetResourceNamesSelect(bindings, stateDetails, cmdCtx) - nsNames = e.tryToGetNamespaceSelect(ctx, bindings, stateDetails) - ) - - // 4. If a given resource name is not on the list anymore, clear it. - if !e.contains(resNames, stateDetails.resourceName) { - stateDetails.resourceName = "" - } - - // 5. If a given namespace is not on the list anymore, clear it. - if !e.contains(nsNames, stateDetails.namespace) { - stateDetails.namespace = "" - } - - // 6. Render all dropdowns and full command preview. - preview := e.buildCommandPreview(stateDetails) - return KubectlCmdBuilderMessage( - stateDetails.dropdownsBlockID, *allVerbsSelect, - WithAdditionalSelects(matchingTypes, resNames, nsNames), - WithAdditionalSections(preview...), - ), nil -} - -func (e *KubectlCmdBuilder) tryToGetResourceNamesSelect(bindings []string, state stateDetails, cmdCtx CommandContext) *api.Select { - if state.resourceType == "" { - return EmptyResourceNameDropdown() - } - cmd := fmt.Sprintf(`%s get %s --ignore-not-found=true -o go-template='{{range .items}}{{.metadata.name}}{{"\n"}}{{end}}'`, kubectlCommandName, state.resourceType) - if state.namespace != "" { - cmd = fmt.Sprintf("%s -n %s", cmd, state.namespace) - } - - out, err := e.kcExecutor.Execute(bindings, cmd, true, cmdCtx) - if err != nil { - e.log.WithField("error", err.Error()).Error("Cannot fetch resource names. Returning empty resource name dropdown.") - return EmptyResourceNameDropdown() - } - - lines := getNonEmptyLines(out) - if len(lines) == 0 { - return EmptyResourceNameDropdown() - } - - return ResourceNamesSelect(overflowSentence(lines), state.resourceName) -} - -func (e *KubectlCmdBuilder) tryToGetNamespaceSelect(ctx context.Context, bindings []string, details stateDetails) *api.Select { - log := e.log.WithFields(logrus.Fields{ - "state": details, - "bindings": bindings, - }) - - resourceDetails, err := e.commandGuard.GetResourceDetails(details.verb, details.resourceType) - if err != nil { - log.WithField("error", err.Error()).Error("Cannot fetch resource details, ignoring namespace dropdown...") - return nil - } - - if !resourceDetails.Namespaced { - log.Debug("Resource is not namespace-scoped, ignore namespace dropdown...") - return nil - } - - allClusterNamespaces, err := e.namespaceLister.List(ctx, metav1.ListOptions{ - Limit: dropdownItemsLimit, - }) - if err != nil { - log.WithField("error", err.Error()).Error("Cannot fetch all available Kubernetes namespaces, ignoring namespace dropdown...") - return nil - } - - var ( - kc = e.merger.MergeAllEnabled(bindings) - allowedNS = kc.AllowedNamespacesPerResource[details.resourceType] - finalNS []dropdownItem - ) - - initialNamespace := newDropdownItem(details.namespace, details.namespace) - initialNamespace = e.appendNamespaceSuffixIfDefault(initialNamespace) - - for _, item := range allClusterNamespaces.Items { - allowed, err := allowedNS.IsAllowed(item.Name) - if err != nil { - log.WithField("namespace", item.Name). - WithField("error", err.Error()). - Error("Cannot check if namespace is allowed, so skipping it.") - continue - } - if !allowed { - log.WithField("namespace", item.Name).Debug("Namespace is not allowed, so skipping it.") - continue - } - - kv := newDropdownItem(item.Name, item.Name) - if initialNamespace.Value == kv.Value { - kv = e.appendNamespaceSuffixIfDefault(kv) - } - - finalNS = append(finalNS, kv) - } - - return ResourceNamespaceSelect(finalNS, initialNamespace) -} - -// UX requirement to append the (namespace) suffix if the namespace is called `default`. -func (e *KubectlCmdBuilder) appendNamespaceSuffixIfDefault(in dropdownItem) dropdownItem { - if in.Name == "default" { - in.Name += " (namespace)" - } - return in -} - -func (e *KubectlCmdBuilder) getEnableKubectlDetails(bindings []string) (verbs []string, resources []string, namespace string) { - enabledKubectls := e.merger.MergeAllEnabled(bindings) - resources = maputil.SortKeys(enabledKubectls.AllowedKubectlResource) - verbs = maputil.SortKeys(enabledKubectls.AllowedKubectlVerb) - - if enabledKubectls.DefaultNamespace == "" { - enabledKubectls.DefaultNamespace = kubectlDefaultNamespace - } - - return verbs, resources, enabledKubectls.DefaultNamespace -} - -// getAllowedResourcesSelectList returns dropdown select with allowed resources for a given verb. -func (e *KubectlCmdBuilder) getAllowedResourcesSelectList(verb string, resources []string, resourceType string) (*api.Select, error) { - allowedResources, err := e.commandGuard.GetAllowedResourcesForVerb(verb, resources) - if err != nil { - return nil, err - } - if len(allowedResources) == 0 { - return nil, nil - } - - allowedResourcesList := make([]string, 0, len(allowedResources)) - for _, item := range allowedResources { - allowedResourcesList = append(allowedResourcesList, item.Name) - } - - return ResourceTypeSelect(allowedResourcesList, resourceType), nil -} - -type stateDetails struct { - dropdownsBlockID string - - verb string - namespace string - resourceType string - resourceName string - filter string -} - -func (e *KubectlCmdBuilder) extractStateDetails(state *slack.BlockActionStates) stateDetails { - if state == nil { - return stateDetails{} - } - - details := stateDetails{} - for blockID, blocks := range state.Values { - if !strings.Contains(blockID, filterPlaintextInputCommand) { - details.dropdownsBlockID = blockID - } - for id, act := range blocks { - id = strings.TrimSpace(id) - - switch id { - case verbsDropdownCommand: - details.verb = act.SelectedOption.Value - case resourceTypesDropdownCommand: - details.resourceType = act.SelectedOption.Value - case resourceNamesDropdownCommand: - details.resourceName = act.SelectedOption.Value - case resourceNamespaceDropdownCommand: - details.namespace = act.SelectedOption.Value - case filterPlaintextInputCommand: - details.filter = act.Value - } - } - } - return details -} - -func (e *KubectlCmdBuilder) contains(matchingTypes *api.Select, resourceType string) bool { - if matchingTypes == nil { - return false - } - - if matchingTypes.InitialOption != nil && matchingTypes.InitialOption.Value == resourceType { - return true - } - - return false -} - -func (e *KubectlCmdBuilder) buildCommandPreview(state stateDetails) []api.Section { - resourceDetails, err := e.commandGuard.GetResourceDetails(state.verb, state.resourceType) - if err != nil { - e.log.WithFields(logrus.Fields{ - "state": state, - "error": err.Error(), - }).Error("Cannot get resource details") - return []api.Section{InternalErrorSection()} - } - - if resourceDetails.SlashSeparatedInCommand && state.resourceName == "" { - // we should not render the command as it will be invalid anyway without the resource name - return nil - } - - cmd := fmt.Sprintf("%s %s %s", kubectlCommandName, state.verb, state.resourceType) - - resourceNameSeparator := " " - if resourceDetails.SlashSeparatedInCommand { - // sometimes kubectl commands requires slash separator, without it, it will not work. For example: - // kubectl logs deploy/ - resourceNameSeparator = "/" - } - - if state.resourceName != "" { - cmd = fmt.Sprintf("%s%s%s", cmd, resourceNameSeparator, state.resourceName) - } - - if resourceDetails.Namespaced && state.namespace != "" { - cmd = fmt.Sprintf("%s -n %s", cmd, state.namespace) - } - - if state.filter != "" { - cmd = fmt.Sprintf("%s --filter=%q", cmd, state.filter) - } - - return PreviewSection(cmd, FilterSection()) -} - -func (e *KubectlCmdBuilder) message(header, msg string) (interactive.CoreMessage, error) { - return interactive.CoreMessage{ - Description: header, - Message: api.Message{ - BaseBody: api.Body{ - Plaintext: msg, - }, - }, - }, nil -} - -func splitByNewLines(c rune) bool { - return c == '\n' || c == '\r' -} - -func overflowSentence(in []string) []string { - for idx := range in { - if len(in[idx]) < 76 { // Maximum length for text field in dropdown is 75 characters. (https://api.slack.com/reference/block-kit/composition-objects#option) - continue - } - - in[idx] = in[idx][:72] + "..." - } - return in -} - -func getNonEmptyLines(in string) []string { - lines := strings.FieldsFunc(in, splitByNewLines) - var out []string - for _, x := range lines { - if x == "" { - continue - } - out = append(out, x) - } - return out -} diff --git a/pkg/execute/kubectl_cmd_builder_msg.go b/pkg/execute/kubectl_cmd_builder_msg.go deleted file mode 100644 index 24f1dffe0..000000000 --- a/pkg/execute/kubectl_cmd_builder_msg.go +++ /dev/null @@ -1,228 +0,0 @@ -package execute - -import ( - "fmt" - - "github.com/kubeshop/botkube/pkg/api" - "github.com/kubeshop/botkube/pkg/bot/interactive" -) - -type ( - // KubectlCmdBuilderOptions holds builder message options. - KubectlCmdBuilderOptions struct { - selects []api.Select - sections []api.Section - } - // KubectlCmdBuilderOption defines option mutator signature. - KubectlCmdBuilderOption func(options *KubectlCmdBuilderOptions) - - // dropdownItem describes the data for the dropdown item. - dropdownItem struct { - Name string - Value string - } -) - -// newDropdownItem returns the dropdownItem instance. -func newDropdownItem(key, value string) dropdownItem { - return dropdownItem{ - Name: key, - Value: value, - } -} - -// dropdownItemsFromSlice is a helper function to create the dropdown items from string slice. -// Name and Value will represent the same data. -func dropdownItemsFromSlice(in []string) []dropdownItem { - var out []dropdownItem - for _, item := range in { - out = append(out, newDropdownItem(item, item)) - } - return out -} - -// WithAdditionalSelects adds additional selects to a given kubectl KubectlCmdBuilderMessage message. -func WithAdditionalSelects(in ...*api.Select) KubectlCmdBuilderOption { - return func(options *KubectlCmdBuilderOptions) { - for _, s := range in { - if s == nil { - continue - } - options.selects = append(options.selects, *s) - } - } -} - -// WithAdditionalSections adds additional sections to a given kubectl KubectlCmdBuilderMessage message. -func WithAdditionalSections(in ...api.Section) KubectlCmdBuilderOption { - return func(options *KubectlCmdBuilderOptions) { - options.sections = append(options.sections, in...) - } -} - -// KubectlCmdBuilderMessage returns message for constructing kubectl command. -func KubectlCmdBuilderMessage(dropdownsBlockID string, verbs api.Select, opts ...KubectlCmdBuilderOption) interactive.CoreMessage { - defaultOpt := KubectlCmdBuilderOptions{ - selects: []api.Select{ - verbs, - }, - } - for _, opt := range opts { - opt(&defaultOpt) - } - - var sections []api.Section - sections = append(sections, api.Section{ - Selects: api.Selects{ - ID: dropdownsBlockID, - Items: defaultOpt.selects, - }, - }) - - sections = append(sections, defaultOpt.sections...) - return interactive.CoreMessage{ - Message: api.Message{ - ReplaceOriginal: true, - OnlyVisibleForYou: true, - Sections: sections, - }, - } -} - -// PreviewSection returns preview command section with Run button. -func PreviewSection(cmd string, input api.LabelInput) []api.Section { - btn := api.ButtonBuilder{} - return []api.Section{ - { - Base: api.Base{ - Body: api.Body{ - CodeBlock: cmd, - }, - }, - PlaintextInputs: api.LabelInputs{ - input, - }, - }, - { - Buttons: api.Buttons{ - btn.ForCommandWithoutDesc(interactive.RunCommandName, cmd, api.ButtonStylePrimary), - }, - }, - } -} - -// InternalErrorSection returns preview command section with Run button. -func InternalErrorSection() api.Section { - return api.Section{ - Base: api.Base{ - Body: api.Body{ - CodeBlock: "Sorry, an internal error occurred while rendering command preview. See the logs for more details.", - }, - }, - } -} - -// FilterSection returns filter input block. -func FilterSection() api.LabelInput { - return api.LabelInput{ - Text: "Filter output", - DispatchedAction: api.DispatchInputActionOnCharacter, - Placeholder: "Filter output by string (optional)", - // the whitespace at the end is required, otherwise we will not recognize the command - // as we will receive: - // kc-cmd-builder --filterinput string - // instead of: - // kc-cmd-builder --filter input string - // TODO: this can be fixed by smarter command parser. - Command: fmt.Sprintf("%s %s ", api.MessageBotNamePlaceholder, filterPlaintextInputCommand), - } -} - -// VerbSelect return drop-down select for kubectl verbs. -func VerbSelect(verbs []string, initialItem string) *api.Select { - return selectDropdown("Select command", verbsDropdownCommand, dropdownItemsFromSlice(verbs), newDropdownItem(initialItem, initialItem)) -} - -// ResourceTypeSelect return drop-down select for kubectl resources types. -func ResourceTypeSelect(resources []string, initialItem string) *api.Select { - return selectDropdown("Select resource", resourceTypesDropdownCommand, dropdownItemsFromSlice(resources), newDropdownItem(initialItem, initialItem)) -} - -// ResourceNamesSelect return drop-down select for kubectl resources names. -func ResourceNamesSelect(names []string, initialItem string) *api.Select { - return selectDropdown("Select resource name", resourceNamesDropdownCommand, dropdownItemsFromSlice(names), newDropdownItem(initialItem, initialItem)) -} - -// ResourceNamespaceSelect return drop-down select for kubectl allowed namespaces. -func ResourceNamespaceSelect(names []dropdownItem, initialNamespace dropdownItem) *api.Select { - return selectDropdown("Select namespace", resourceNamespaceDropdownCommand, names, initialNamespace) -} - -func selectDropdown(name, cmd string, items []dropdownItem, initialItem dropdownItem) *api.Select { - if len(items) == 0 { - return nil - } - - var opts []api.OptionItem - foundInitialOptOnList := false - for _, item := range items { - if item.Value == "" || item.Name == "" { - continue - } - - if initialItem.Value == item.Value && initialItem.Name == item.Name { - foundInitialOptOnList = true - } - - opts = append(opts, api.OptionItem{ - Name: item.Name, - Value: item.Value, - }) - } - - var initialOption *api.OptionItem - if foundInitialOptOnList { - initialOption = &api.OptionItem{ - Name: initialItem.Name, - Value: initialItem.Value, - } - } - - if len(opts) == 0 { - return nil - } - - return &api.Select{ - Name: name, - Command: fmt.Sprintf("%s %s", api.MessageBotNamePlaceholder, cmd), - InitialOption: initialOption, - OptionGroups: []api.OptionGroup{ - { - Name: name, - Options: opts, - }, - }, - } -} - -// EmptyResourceNameDropdown returns a select that simulates an empty one. -// Normally, Slack doesn't allow to return a static select with no options. -// This is a workaround to send a dropdown that it's rendered even if empty. -// We use that to preserve a proper order in displayed dropdowns. -// -// How it works under the hood: -// 1. This select is converted to external data source (https://api.slack.com/reference/block-kit/block-elements#external_select) -// 2. We change the `min_query_length` to 0 to remove th "Type minimum of 3 characters to see options" message. -// 3. Our backend doesn't return any options, so you see "No result". -// 4. We don't set the command, so the ID of this select is always randomized by Slack server. -// As a result, the dropdown value is not cached, and we avoid problem with showing the outdated value. -func EmptyResourceNameDropdown() *api.Select { - return &api.Select{ - Type: api.ExternalSelect, - Name: "No resources found", - InitialOption: &api.OptionItem{ - Name: "No resources found", - Value: "no-resources", - }, - } -} diff --git a/pkg/execute/kubectl_cmd_builder_test.go b/pkg/execute/kubectl_cmd_builder_test.go deleted file mode 100644 index 74cd1ff3c..000000000 --- a/pkg/execute/kubectl_cmd_builder_test.go +++ /dev/null @@ -1,530 +0,0 @@ -package execute_test - -import ( - "context" - "errors" - "fmt" - "strings" - "testing" - - "github.com/slack-go/slack" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/kubeshop/botkube/internal/loggerx" - "github.com/kubeshop/botkube/pkg/api" - "github.com/kubeshop/botkube/pkg/bot/interactive" - "github.com/kubeshop/botkube/pkg/config" - "github.com/kubeshop/botkube/pkg/execute" - "github.com/kubeshop/botkube/pkg/execute/kubectl" -) - -const testingBotName = "@BKTesting" - -func TestCommandPreview(t *testing.T) { - fixBindings := []string{"kc-read-only", "kc-delete-pod"} - - tests := []struct { - name string - args []string - - expMsg interactive.CoreMessage - }{ - { - name: "Print all dropdowns and full command on verb change", - args: strings.Fields("kc-cmd-builder --verbs"), - - expMsg: fixStateBuilderMessage("kubectl get pods nginx2 -n default", "@BKTesting kubectl get pods nginx2 -n default", fixAllDropdown(true)...), - }, - { - name: "Print all dropdowns and command without the resource name on resource type change", - args: strings.Fields("kc-cmd-builder --resource-type"), - - expMsg: fixStateBuilderMessage("kubectl get pods -n default", "@BKTesting kubectl get pods -n default", fixAllDropdown(false)...), - }, - { - name: "Print all dropdowns and full command on resource name change", - args: strings.Fields("kc-cmd-builder --resource-name"), - - expMsg: fixStateBuilderMessage("kubectl get pods nginx2 -n default", "@BKTesting kubectl get pods nginx2 -n default", fixAllDropdown(true)...), - }, - { - name: "Print all dropdowns and command without the resource name on namespace change", - args: strings.Fields("kc-cmd-builder --namespace"), - - expMsg: fixStateBuilderMessage("kubectl get pods -n default", "@BKTesting kubectl get pods -n default", fixAllDropdown(false)...), - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // given - var ( - expKubectlCmd = `kubectl get pods --ignore-not-found=true -o go-template='{{range .items}}{{.metadata.name}}{{"\n"}}{{end}}' -n default` - state = fixStateForAllDropdowns() - kcExecutor = &fakeKcExecutor{} - nsLister = &fakeNamespaceLister{} - kcMerger = newFakeKcMerger([]string{"get", "describe"}, []string{"deployments", "pods"}) - ) - - kcCmdBuilderExecutor := execute.NewKubectlCmdBuilder(loggerx.NewNoop(), kcMerger, kcExecutor, nsLister, &kubectl.FakeCommandGuard{}) - - // when - gotMsg, err := kcCmdBuilderExecutor.Do(context.Background(), tc.args, config.SocketSlackCommPlatformIntegration, fixBindings, state, "header", execute.CommandContext{}) - gotMsg.ReplaceBotNamePlaceholder(testingBotName) - - // then - require.NoError(t, err) - assert.Equal(t, tc.expMsg, gotMsg) - assert.Equal(t, expKubectlCmd, kcExecutor.command) - assert.True(t, kcExecutor.isAuthed) - assert.Equal(t, fixBindings, kcExecutor.bindings) - }) - } -} - -func TestCommandBuilderCanHandleAndGetPrefix(t *testing.T) { - tests := []struct { - name string - args []string - - expPrefix string - expCanHandle bool - }{ - { - name: "Dropdown verbs", - args: strings.Fields("kc-cmd-builder --verbs my-verb"), - - expCanHandle: true, - expPrefix: "kc-cmd-builder --verbs", - }, - { - name: "Dropdown resource type", - args: strings.Fields("kc-cmd-builder --resource-type my-resource-type"), - - expCanHandle: true, - expPrefix: "kc-cmd-builder --resource-type", - }, - { - name: "Dropdown resource name", - args: strings.Fields("kc-cmd-builder --resource-name my-resource-name"), - - expCanHandle: true, - expPrefix: "kc-cmd-builder --resource-name", - }, - { - name: "Dropdown namespace", - args: strings.Fields("kc-cmd-builder --namespace my-namespace"), - - expCanHandle: true, - expPrefix: "kc-cmd-builder --namespace", - }, - { - name: "Dropdown namespace", - args: strings.Fields("kc-cmd-builder --namespace my-namespace other-arg-but-we-dont-care"), - - expCanHandle: true, - expPrefix: "kc-cmd-builder --namespace", - }, - { - name: "Kubectl full command", - args: strings.Fields("kubectl"), - - expCanHandle: true, - expPrefix: "kubectl", - }, - { - name: "Kubectl full command", - args: strings.Fields("kubectl get pod"), - - expCanHandle: false, - expPrefix: "", - }, - { - name: "Unknown command", - args: strings.Fields("helm"), - - expCanHandle: false, - expPrefix: "", - }, - { - name: "Wrong command", - args: strings.Fields("kc-cmd-builder"), - - expCanHandle: false, - expPrefix: "", - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // given - kcCmdBuilderExecutor := execute.NewKubectlCmdBuilder(nil, nil, nil, nil, &kubectl.FakeCommandGuard{}) - - // when - gotCanHandle := kcCmdBuilderExecutor.CanHandle(tc.args) - gotPrefix := kcCmdBuilderExecutor.GetCommandPrefix(tc.args) - - // then - assert.Equal(t, tc.expCanHandle, gotCanHandle) - assert.Equal(t, tc.expPrefix, gotPrefix) - }) - } -} - -func TestErrorUserMessageOnPlatformsOtherThanSocketSlack(t *testing.T) { - platforms := []config.CommPlatformIntegration{ - config.SlackCommPlatformIntegration, - config.MattermostCommPlatformIntegration, - config.TeamsCommPlatformIntegration, - config.DiscordCommPlatformIntegration, - config.ElasticsearchCommPlatformIntegration, - config.WebhookCommPlatformIntegration, - } - for _, platform := range platforms { - t.Run(fmt.Sprintf("Should ignore %s", platform), func(t *testing.T) { - // given - const cmdHeader = "header" - kcCmdBuilderExecutor := execute.NewKubectlCmdBuilder(loggerx.NewNoop(), nil, nil, nil, nil) - - // when - gotMsg, err := kcCmdBuilderExecutor.Do(context.Background(), []string{"kc"}, platform, nil, nil, cmdHeader, execute.CommandContext{}) - gotMsg.ReplaceBotNamePlaceholder(testingBotName) - - // then - require.NoError(t, err) - assert.Equal(t, interactive.CoreMessage{ - Description: cmdHeader, - Message: api.Message{ - BaseBody: api.Body{ - Plaintext: "Please specify the kubectl command", - }, - }, - }, gotMsg) - }) - } -} - -func TestShouldReturnInitialMessage(t *testing.T) { - // given - var ( - kcMerger = newFakeKcMerger([]string{"get", "describe"}, []string{"deployments", "pods"}) - kcCmdBuilderExecutor = execute.NewKubectlCmdBuilder(loggerx.NewNoop(), kcMerger, nil, nil, &kubectl.FakeCommandGuard{}) - expMsg = fixInitialBuilderMessage() - ) - - // when command args are not specified - cmd := []string{"kc-cmd-builder"} - gotMsg, err := kcCmdBuilderExecutor.Do(context.Background(), cmd, config.SocketSlackCommPlatformIntegration, nil, nil, "cmdHeader", execute.CommandContext{}) - gotMsg.ReplaceBotNamePlaceholder(testingBotName) - - // then - require.NoError(t, err) - - require.Len(t, gotMsg.Sections, 1) - assert.NotEmpty(t, gotMsg.Sections[0].Selects.ID) // assert that we fill that property - gotMsg.Sections[0].Selects.ID = "" // zero that before comparison, as this is UUID that it's different in each test execution. - - assert.Equal(t, expMsg, gotMsg) -} - -func TestShouldNotPrintTheResourceNameIfKubectlExecutorFails(t *testing.T) { - // given - var ( - state = fixStateForAllDropdowns() - kcExecutor = &fakeErrorKcExecutor{} - nsLister = &fakeNamespaceLister{} - kcMerger = newFakeKcMerger([]string{"get", "describe"}, []string{"deployments", "pods"}) - args = []string{"kc-cmd-builder", "--verbs"} - expMsg = fixStateBuilderMessage("kubectl get pods -n default", "@BKTesting kubectl get pods -n default", fixVerbsDropdown(), fixResourceTypeDropdown(), fixEmptyResourceNamesDropdown(), fixNamespaceDropdown()) - ) - - kcCmdBuilderExecutor := execute.NewKubectlCmdBuilder(loggerx.NewNoop(), kcMerger, kcExecutor, nsLister, &kubectl.FakeCommandGuard{}) - - // when - gotMsg, err := kcCmdBuilderExecutor.Do(context.Background(), args, config.SocketSlackCommPlatformIntegration, []string{"kc-read-only"}, state, "header", execute.CommandContext{}) - gotMsg.ReplaceBotNamePlaceholder(testingBotName) - - // then - require.NoError(t, err) - assert.Equal(t, expMsg, gotMsg) -} - -func fixStateForAllDropdowns() *slack.BlockActionStates { - return &slack.BlockActionStates{ - Values: map[string]map[string]slack.BlockAction{ - "dropdown-block-id-403aca17d958": { - "kc-cmd-builder --resource-name": { - SelectedOption: slack.OptionBlockObject{ - Value: "nginx2", - }, - }, - "kc-cmd-builder --resource-type": slack.BlockAction{ - SelectedOption: slack.OptionBlockObject{ - Value: "pods", - }, - }, - "kc-cmd-builder --verbs": slack.BlockAction{ - SelectedOption: slack.OptionBlockObject{ - Value: "get", - }, - }, - }, - }, - } -} - -func fixInitialBuilderMessage() interactive.CoreMessage { - verbsDropdown := fixVerbsDropdown() - verbsDropdown.InitialOption = nil // initial message shouldn't have anything selected. - return interactive.CoreMessage{ - Message: api.Message{ - Sections: []api.Section{ - { - Selects: api.Selects{ - Items: []api.Select{ - verbsDropdown, - }, - }, - }, - }, - OnlyVisibleForYou: true, - ReplaceOriginal: false, - }, - } -} - -func fixVerbsDropdown() api.Select { - return api.Select{ - Name: "Select command", - Command: "@BKTesting kc-cmd-builder --verbs", - InitialOption: &api.OptionItem{ - Name: "get", - Value: "get", - }, - OptionGroups: []api.OptionGroup{ - { - Name: "Select command", - Options: []api.OptionItem{ - { - Name: "describe", - Value: "describe", - }, - { - Name: "get", - Value: "get", - }, - }, - }, - }, - } -} - -func fixResourceTypeDropdown() api.Select { - return api.Select{ - Name: "Select resource", - Command: "@BKTesting kc-cmd-builder --resource-type", - InitialOption: &api.OptionItem{ - Name: "pods", - Value: "pods", - }, - OptionGroups: []api.OptionGroup{ - { - Name: "Select resource", - Options: []api.OptionItem{ - { - Name: "deployments", - Value: "deployments", - }, - { - Name: "pods", - Value: "pods", - }, - }, - }, - }, - } -} - -func fixNamespaceDropdown() api.Select { - return api.Select{ - Name: "Select namespace", - Command: "@BKTesting kc-cmd-builder --namespace", - OptionGroups: []api.OptionGroup{ - { - Name: "Select namespace", - Options: []api.OptionItem{ - { - Name: "default (namespace)", - Value: "default", - }, - }, - }, - }, - InitialOption: &api.OptionItem{ - Name: "default (namespace)", - Value: "default", - }, - } -} - -func fixEmptyResourceNamesDropdown() api.Select { - return api.Select{ - Name: "No resources found", - Type: api.ExternalSelect, - InitialOption: &api.OptionItem{ - Name: "No resources found", - Value: "no-resources", - }, - } -} - -func fixResourceNamesDropdown(includeInitialOpt bool) api.Select { - var opt *api.OptionItem - if includeInitialOpt { - opt = &api.OptionItem{ - Name: "nginx2", - Value: "nginx2", - } - } - - return api.Select{ - Name: "Select resource name", - Command: "@BKTesting kc-cmd-builder --resource-name", - InitialOption: opt, - OptionGroups: []api.OptionGroup{ - { - Name: "Select resource name", - Options: []api.OptionItem{ - { - Name: "nginx2", - Value: "nginx2", - }, - { - Name: "grafana", - Value: "grafana", - }, - { - Name: "argo", - Value: "argo", - }, - }, - }, - }, - } -} - -func fixAllDropdown(includeResourceName bool) []api.Select { - return []api.Select{ - fixVerbsDropdown(), - fixResourceTypeDropdown(), - fixResourceNamesDropdown(includeResourceName), - fixNamespaceDropdown(), - } -} - -func fixStateBuilderMessage(kcCommandPreview, kcCommand string, dropdowns ...api.Select) interactive.CoreMessage { - return interactive.CoreMessage{ - Message: api.Message{ - Sections: []api.Section{ - { - Selects: api.Selects{ - ID: "dropdown-block-id-403aca17d958", // It's important to have the same ID as we have in fixture state object. - Items: dropdowns, - }, - }, - { - Base: api.Base{ - Body: api.Body{ - CodeBlock: kcCommandPreview, - }, - }, - PlaintextInputs: api.LabelInputs{ - api.LabelInput{ - Command: "@BKTesting kc-cmd-builder --filter-query ", - DispatchedAction: api.DispatchInputActionOnCharacter, - Text: "Filter output", - Placeholder: "Filter output by string (optional)", - }, - }, - }, - { - Buttons: api.Buttons{ - api.Button{ - Name: "Run command", - Command: kcCommand, - Style: "primary", - }, - }, - }, - }, - OnlyVisibleForYou: true, - ReplaceOriginal: true, - }, - } -} - -type fakeKcExecutor struct { - isAuthed bool - command string - bindings []string -} - -func (r *fakeKcExecutor) Execute(bindings []string, command string, isAuthChannel bool, _ execute.CommandContext) (string, error) { - r.bindings = bindings - r.command = command - r.isAuthed = isAuthChannel - - return "nginx2\ngrafana\nargo", nil -} - -type fakeErrorKcExecutor struct{} - -func (r *fakeErrorKcExecutor) Execute(_ []string, _ string, _ bool, _ execute.CommandContext) (string, error) { - return "", errors.New("fake error") -} - -type fakeKcMerger struct { - allowedVerbs []string - allowedResources []string -} - -func newFakeKcMerger(allowedVerbs []string, allowedResources []string) *fakeKcMerger { - return &fakeKcMerger{allowedVerbs: allowedVerbs, allowedResources: allowedResources} -} - -func (r *fakeKcMerger) MergeAllEnabled(_ []string) kubectl.EnabledKubectl { - verbs := map[string]struct{}{} - for _, name := range r.allowedVerbs { - verbs[name] = struct{}{} - } - resources := map[string]struct{}{} - for _, name := range r.allowedResources { - resources[name] = struct{}{} - } - resourceNamespaces := map[string]config.RegexConstraints{} - for _, name := range r.allowedResources { - resourceNamespaces[name] = config.RegexConstraints{ - Include: []string{"default"}, - } - } - return kubectl.EnabledKubectl{ - AllowedKubectlVerb: verbs, - AllowedKubectlResource: resources, - AllowedNamespacesPerResource: resourceNamespaces, - } -} - -type fakeNamespaceLister struct{} - -func (f *fakeNamespaceLister) List(_ context.Context, _ metav1.ListOptions) (*corev1.NamespaceList, error) { - return &corev1.NamespaceList{ - Items: []corev1.Namespace{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "default", - }, - }, - }, - }, nil -} diff --git a/pkg/execute/kubectl_test.go b/pkg/execute/kubectl_test.go deleted file mode 100644 index 405921a05..000000000 --- a/pkg/execute/kubectl_test.go +++ /dev/null @@ -1,495 +0,0 @@ -package execute - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/kubeshop/botkube/internal/loggerx" - "github.com/kubeshop/botkube/pkg/config" - "github.com/kubeshop/botkube/pkg/execute/kubectl" - "github.com/kubeshop/botkube/pkg/ptr" -) - -func TestKubectlExecuteErrors(t *testing.T) { - tests := []struct { - name string - - command string - clusterName string - channelNotAuthorized bool - kubectlCfg config.Kubectl - expKubectlExecuted bool - expErr string - }{ - { - name: "Should forbid execution from not authorized channel when restrictions are enabled", - - command: "kubectl get pod --cluster-name test", - clusterName: "test", - channelNotAuthorized: true, - kubectlCfg: config.Kubectl{ - Enabled: true, - Namespaces: config.RegexConstraints{ - Include: []string{"default"}, - }, - RestrictAccess: ptr.Bool(true), - Commands: config.Commands{ - Verbs: []string{"get"}, - }, - }, - - expErr: "Sorry, this channel is not authorized to execute kubectl command on cluster 'test'.", - }, - { - name: "Should forbid execution if resource is not allowed in config", - - command: "kubectl get pod -n foo", - kubectlCfg: config.Kubectl{ - Enabled: true, - Namespaces: config.RegexConstraints{ - Include: []string{"foo"}, - }, - Commands: config.Commands{ - Verbs: []string{"get"}, - Resources: nil, - }, - }, - expErr: "Sorry, the kubectl command is not authorized to work with 'pod' resources in the 'foo' Namespace on cluster 'test'. Use 'list executors' to see allowed executors.", - }, - { - name: "Should forbid execution if namespace is not allowed in config", - - command: "kubectl get pod", - kubectlCfg: config.Kubectl{ - Enabled: true, - Namespaces: config.RegexConstraints{ - Include: nil, // no namespace allowed. - }, - Commands: config.Commands{ - Verbs: []string{"get"}, - Resources: []string{"pod"}, - }, - }, - - expErr: "Sorry, the kubectl 'get' command cannot be executed in the 'default' Namespace on cluster 'test'. Use 'list executors' to see allowed executors.", - }, - { - name: "Should use default Namespace from config if not specified in command", - - command: "kubectl get pod", - kubectlCfg: config.Kubectl{ - Enabled: true, - DefaultNamespace: "from-config", - Namespaces: config.RegexConstraints{ - Include: nil, // forbid `from-config` to get a suitable error message. - }, - Commands: config.Commands{ - Verbs: []string{"get"}, - Resources: []string{"pod"}, - }, - }, - - expErr: "Sorry, the kubectl 'get' command cannot be executed in the 'from-config' Namespace on cluster 'test'. Use 'list executors' to see allowed executors.", - }, - { - name: "Should explicitly use 'default' Namespace if not specified both in command and config", - - command: "kubectl get pod", - kubectlCfg: config.Kubectl{ - Enabled: true, - Namespaces: config.RegexConstraints{ - Include: nil, // forbid `default` to get a suitable error message. - }, - Commands: config.Commands{ - Verbs: []string{"get"}, - Resources: []string{"pod"}, - }, - }, - - expErr: "Sorry, the kubectl 'get' command cannot be executed in the 'default' Namespace on cluster 'test'. Use 'list executors' to see allowed executors.", - }, - { - name: "Should forbid execution in not allowed namespace", - - command: "kubectl get pod -n team-b", - kubectlCfg: config.Kubectl{ - Enabled: true, - Namespaces: config.RegexConstraints{ - Include: []string{"team-a"}, - }, - Commands: config.Commands{ - Verbs: []string{"get"}, - Resources: []string{"pod"}, - }, - }, - - expErr: "Sorry, the kubectl 'get' command cannot be executed in the 'team-b' Namespace on cluster 'test'. Use 'list executors' to see allowed executors.", - }, - { - name: "Should forbid execution if all namespace are allowed but command namespace is explicitly ignored in config", - - command: "kubectl get pod -n team-b", - kubectlCfg: config.Kubectl{ - Enabled: true, - Namespaces: config.RegexConstraints{ - Include: []string{config.AllNamespaceIndicator}, - Exclude: []string{"team-b"}, - }, - Commands: config.Commands{ - Verbs: []string{"get"}, - Resources: []string{"pod"}, - }, - }, - - expErr: "Sorry, the kubectl 'get' command cannot be executed in the 'team-b' Namespace on cluster 'test'. Use 'list executors' to see allowed executors.", - }, - { - name: "Should forbid execution for all Namespaces", - - command: "kubectl get pod -A", - kubectlCfg: config.Kubectl{ - Enabled: true, - Namespaces: config.RegexConstraints{ - Include: []string{"team-a"}, - }, - Commands: config.Commands{ - Verbs: []string{"get"}, - Resources: []string{"pod"}, - }, - }, - - expErr: "Sorry, the kubectl 'get' command cannot be executed for all Namespaces on cluster 'test'. Use 'list executors' to see allowed executors.", - }, - { - name: "Known limitation (since v0.12.4): Return error if flag is added before resource name", - - command: "kubectl get -n team-a pod", - kubectlCfg: config.Kubectl{ - Enabled: true, - Namespaces: config.RegexConstraints{ - Include: []string{"team-a"}, - }, - Commands: config.Commands{ - Verbs: []string{"get"}, - Resources: []string{"pod"}, - }, - }, - - expKubectlExecuted: false, - expErr: "Please specify the resource name after the verb, and all flags after the resource name. Format [flags]", - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // given - cfg := fixCfgWithKubectlExecutor(t, tc.kubectlCfg) - merger := kubectl.NewMerger(cfg.Executors) - kcChecker := kubectl.NewChecker(nil) - - wasKubectlExecuted := false - - executor := NewKubectl(loggerx.NewNoop(), cfg, merger, kcChecker, cmdCombinedFunc(func(command string, args []string) (string, error) { - wasKubectlExecuted = true - return "kubectl executed", nil - })) - - // when - canHandle := executor.CanHandle(strings.Fields(strings.TrimSpace(tc.command))) - gotOutMsg, err := executor.Execute(fixBindingsNames, tc.command, !tc.channelNotAuthorized, CommandContext{ProvidedClusterName: tc.clusterName, ClusterName: tc.clusterName}) - - // then - assert.True(t, canHandle, "it should be able to handle the execution") - assert.True(t, IsExecutionCommandError(err)) - assert.False(t, wasKubectlExecuted) - assert.Empty(t, gotOutMsg) - assert.EqualError(t, err, tc.expErr) - }) - } -} - -func TestKubectlExecute(t *testing.T) { - tests := []struct { - name string - - command string - channelNotAuthorized bool - kubectlCfg config.Kubectl - expOutMsg string - }{ - { - name: "Should all execution if resource is missing, so kubectl can validate it further", - - command: "kubectl get", - kubectlCfg: config.Kubectl{ - Enabled: true, - Namespaces: config.RegexConstraints{ - Include: []string{"default"}, - }, - Commands: config.Commands{ - Verbs: []string{"get"}, - Resources: nil, - }, - }, - expOutMsg: "kubectl executed", - }, - { - name: "Should allow execution if verb, resource, and all namespaces are allowed", - - command: "kubectl get pod", - kubectlCfg: config.Kubectl{ - Enabled: true, - Namespaces: config.RegexConstraints{ - Include: []string{config.AllNamespaceIndicator}, - }, - Commands: config.Commands{ - Verbs: []string{"get"}, - Resources: []string{"pod"}, - }, - }, - - expOutMsg: "kubectl executed", - }, - { - name: "Should allow execution if verb, resource, and a given namespace are allowed", - - command: "kubectl get pod -n team-a", - kubectlCfg: config.Kubectl{ - Enabled: true, - Namespaces: config.RegexConstraints{ - Include: []string{"team-a"}, - }, - Commands: config.Commands{ - Verbs: []string{"get"}, - Resources: []string{"pod"}, - }, - }, - - expOutMsg: "kubectl executed", - }, - { - name: "Should allow execution from not authorized channel if restrictions are disabled", - - command: "kubectl get pod -n team-a", - channelNotAuthorized: true, - kubectlCfg: config.Kubectl{ - Enabled: true, - Namespaces: config.RegexConstraints{ - Include: []string{"team-a"}, - }, - Commands: config.Commands{ - Verbs: []string{"get"}, - Resources: []string{"pod"}, - }, - }, - - expOutMsg: "kubectl executed", - }, - { - name: "Should allow execution from not authorized channel if restrictions are disabled", - - command: "kubectl get pod/name-foo-42 -n team-a", - channelNotAuthorized: true, - kubectlCfg: config.Kubectl{ - Enabled: true, - Namespaces: config.RegexConstraints{ - Include: []string{"team-a"}, - }, - Commands: config.Commands{ - Verbs: []string{"get"}, - Resources: []string{"pod"}, - }, - }, - - expOutMsg: "kubectl executed", - }, - { - name: "Should allow execution for all Namespaces", - - command: "kubectl get pod/name-foo-42 -n team-a", - kubectlCfg: config.Kubectl{ - Enabled: true, - Namespaces: config.RegexConstraints{ - Include: []string{"team-a"}, - }, - Commands: config.Commands{ - Verbs: []string{"get"}, - Resources: []string{"pod"}, - }, - }, - - expOutMsg: "kubectl executed", - }, - { - name: "Should all execution for all Namespaces", - - command: "kubectl get pod -A", - kubectlCfg: config.Kubectl{ - Enabled: true, - Namespaces: config.RegexConstraints{ - Include: []string{".*"}, - }, - Commands: config.Commands{ - Verbs: []string{"get"}, - Resources: []string{"pod"}, - }, - }, - - expOutMsg: "kubectl executed", - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // given - cfg := fixCfgWithKubectlExecutor(t, tc.kubectlCfg) - merger := kubectl.NewMerger(cfg.Executors) - kcChecker := kubectl.NewChecker(nil) - - wasKubectlExecuted := false - - executor := NewKubectl(loggerx.NewNoop(), cfg, merger, kcChecker, cmdCombinedFunc(func(command string, args []string) (string, error) { - wasKubectlExecuted = true - return "kubectl executed", nil - })) - - // when - canHandle := executor.CanHandle(strings.Fields(strings.TrimSpace(tc.command))) - gotOutMsg, err := executor.Execute(fixBindingsNames, tc.command, !tc.channelNotAuthorized, CommandContext{}) - - // then - assert.True(t, canHandle, "it should be able to handle the execution") - require.NoError(t, err) - assert.True(t, wasKubectlExecuted) - assert.Equal(t, tc.expOutMsg, gotOutMsg) - }) - } -} - -func TestKubectlCanHandle(t *testing.T) { - // given - command := "kubectl get pod --cluster-name test" - kubectlCfg := config.Kubectl{ - Enabled: true, - Namespaces: config.RegexConstraints{ - Include: []string{"team-a"}, - }, - Commands: config.Commands{ - Verbs: []string{"get"}, - Resources: []string{"pod"}, - }, - } - expCanHandle := true - - cfg := fixCfgWithKubectlExecutor(t, kubectlCfg) - merger := kubectl.NewMerger(cfg.Executors) - kcChecker := kubectl.NewChecker(nil) - - executor := NewKubectl(loggerx.NewNoop(), config.Config{}, merger, kcChecker, nil) - - // when - canHandle := executor.CanHandle(strings.Fields(strings.TrimSpace(command))) - - // then - assert.Equal(t, expCanHandle, canHandle) -} - -func TestKubectlGetCommandPrefix(t *testing.T) { - tests := []struct { - name string - command string - expected string - }{ - { - name: "Should get proper command with k8s prefix kubectl", - command: "kubectl get pods --cluster-name test", - expected: "kubectl get", - }, - { - name: "Should get proper command with k8s prefix kc", - command: "kubectl get pods --cluster-name test", - expected: "kubectl get", - }, - { - name: "Should get proper command with k8s prefix k", - command: "k get pods --cluster-name test", - expected: "k get", - }, - } - kubectlCfg := config.Kubectl{ - Enabled: true, - Namespaces: config.RegexConstraints{ - Include: []string{"team-a"}, - }, - Commands: config.Commands{ - Verbs: []string{"get"}, - Resources: []string{"pod"}, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - cfg := fixCfgWithKubectlExecutor(t, kubectlCfg) - merger := kubectl.NewMerger(cfg.Executors) - kcChecker := kubectl.NewChecker(nil) - executor := NewKubectl(loggerx.NewNoop(), config.Config{}, merger, kcChecker, nil) - - args := strings.Fields(tc.command) - verb := executor.GetCommandPrefix(args) - - assert.Equal(t, tc.expected, verb) - }) - } -} - -func TestKubectlGetArgsWithoutAlias(t *testing.T) { - // given - command := "kubectl get pods --cluster-name test" - expected := "get pods --cluster-name test" - kubectlCfg := config.Kubectl{ - Enabled: true, - Namespaces: config.RegexConstraints{ - Include: []string{"team-a"}, - }, - Commands: config.Commands{ - Verbs: []string{"get"}, - Resources: []string{"pod"}, - }, - } - cfg := fixCfgWithKubectlExecutor(t, kubectlCfg) - merger := kubectl.NewMerger(cfg.Executors) - kcChecker := kubectl.NewChecker(nil) - executor := NewKubectl(loggerx.NewNoop(), config.Config{}, merger, kcChecker, nil) - - // when - verb, err := executor.getArgsWithoutAlias(command) - - // then - require.NoError(t, err) - assert.Equal(t, expected, strings.Join(verb, " ")) -} - -var fixBindingsNames = []string{"default"} - -func fixCfgWithKubectlExecutor(t *testing.T, executor config.Kubectl) config.Config { - t.Helper() - - return config.Config{ - Settings: config.Settings{ - ClusterName: "test", - }, - Executors: map[string]config.Executors{ - "default": { - Kubectl: executor, - }, - }, - } -} - -// cmdCombinedFunc type is an adapter to allow the use of ordinary functions as command handlers. -type cmdCombinedFunc func(command string, args []string) (string, error) - -func (f cmdCombinedFunc) RunCombinedOutput(command string, args []string) (string, error) { - return f(command, args) -} diff --git a/pkg/execute/mapping.go b/pkg/execute/mapping.go index a28bb205d..a5c2165b8 100644 --- a/pkg/execute/mapping.go +++ b/pkg/execute/mapping.go @@ -57,12 +57,7 @@ type CommandContext struct { // ProvidedClusterNameEqualOrEmpty returns true when provided cluster name is empty // or when provided cluster name is equal to cluster name func (cmdCtx CommandContext) ProvidedClusterNameEqualOrEmpty() bool { - return cmdCtx.ProvidedClusterName == "" || cmdCtx.ProvidedClusterNameEqual() -} - -// ProvidedClusterNameEqual returns true when provided cluster name is equal to cluster name -func (cmdCtx CommandContext) ProvidedClusterNameEqual() bool { - return cmdCtx.ProvidedClusterName == cmdCtx.ClusterName + return cmdCtx.ProvidedClusterName == "" || cmdCtx.ProvidedClusterName == cmdCtx.ClusterName } // FeatureName defines the name and aliases for a feature diff --git a/pkg/execute/plugin_discovery.go b/pkg/execute/plugin_discovery.go new file mode 100644 index 000000000..8f52163c3 --- /dev/null +++ b/pkg/execute/plugin_discovery.go @@ -0,0 +1,17 @@ +package execute + +var staticPluginDiscovery = map[string]string{ + "kubectl": "No `kubectl` commands are enabled in this channel. To learn how to enable them, visit https://docs.botkube.io/configuration/executor/kubectl", + "helm": "No `helm` commands are enabled in this channel. To learn how to enable them, visit https://docs.botkube.io/configuration/executor/helm", +} + +// GetInstallHelpForKnownPlugin returns install help for a known plugin. +func GetInstallHelpForKnownPlugin(args []string) (string, bool) { + if len(args) == 0 { + return "", false + } + + cmdName := args[0] + help, found := staticPluginDiscovery[cmdName] + return help, found +} diff --git a/pkg/execute/source.go b/pkg/execute/source.go index 52976695d..5506c7a8a 100644 --- a/pkg/execute/source.go +++ b/pkg/execute/source.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "fmt" - "reflect" "text/tabwriter" "github.com/sirupsen/logrus" @@ -22,8 +21,6 @@ var ( } ) -const kubernetesBuiltinSourceName = "kubernetes" - // SourceExecutor executes all commands that are related to sources. type SourceExecutor struct { log logrus.FieldLogger @@ -68,11 +65,6 @@ func (e *SourceExecutor) TabularOutput(bindings []string) string { for name, plugin := range s.Plugins { sources[name] = plugin.Enabled } - - // TODO: Remove once we extract the source to a separate plugin - if !reflect.DeepEqual(s.Kubernetes, config.KubernetesSource{}) { - sources[kubernetesBuiltinSourceName] = true - } } buf := new(bytes.Buffer) diff --git a/pkg/execute/source_test.go b/pkg/execute/source_test.go index c568cf960..8c71425a5 100644 --- a/pkg/execute/source_test.go +++ b/pkg/execute/source_test.go @@ -10,7 +10,6 @@ import ( "github.com/kubeshop/botkube/internal/loggerx" "github.com/kubeshop/botkube/pkg/config" - "github.com/kubeshop/botkube/pkg/ptr" ) func TestSourceExecutor(t *testing.T) { @@ -27,11 +26,9 @@ func TestSourceExecutor(t *testing.T) { Sources: map[string]config.Sources{ "kubectl-team-a": { DisplayName: "kubectl-team-a", - Kubernetes: config.KubernetesSource{ - Recommendations: config.Recommendations{ - Pod: config.PodRecommendations{ - NoLatestImageTag: ptr.Bool(true), - }, + Plugins: map[string]config.Plugin{ + "kubernetes": { + Enabled: true, }, }, }, @@ -69,21 +66,17 @@ func TestSourceExecutor(t *testing.T) { Sources: map[string]config.Sources{ "kubectl-team-a": { DisplayName: "kubectl-team-a", - Kubernetes: config.KubernetesSource{ - Recommendations: config.Recommendations{ - Pod: config.PodRecommendations{ - NoLatestImageTag: ptr.Bool(true), - }, + Plugins: map[string]config.Plugin{ + "kubernetes": { + Enabled: true, }, }, }, "kubectl-team-b": { DisplayName: "kubectl-team-b", - Kubernetes: config.KubernetesSource{ - Recommendations: config.Recommendations{ - Pod: config.PodRecommendations{ - LabelsSet: ptr.Bool(true), - }, + Plugins: map[string]config.Plugin{ + "kubernetes": { + Enabled: true, }, }, }, diff --git a/test/e2e/bots_test.go b/test/e2e/bots_test.go index feb97c201..5f54d81ad 100644 --- a/test/e2e/bots_test.go +++ b/test/e2e/bots_test.go @@ -5,6 +5,7 @@ package e2e import ( "context" "fmt" + "regexp" "strings" "testing" "time" @@ -79,17 +80,18 @@ const ( welcomeText = "Let the tests begin 🤞" pollInterval = time.Second globalConfigMapName = "botkube-global-config" - discordInvalidCmd = "You must specify the type of resource to get. Use \"kubectl api-resources\" for a complete list of supported resources.\n\nerror: Required resource not specified.\nUse \"kubectl explain \" for a detailed description of that resource (e.g. kubectl explain pods).\nSee 'kubectl get -h' for help and examples\nexit status 1" ) var ( - slackInvalidCmd = heredoc.Doc(` + discordInvalidCmd = heredoc.Doc(` You must specify the type of resource to get. Use "kubectl api-resources" for a complete list of supported resources. error: Required resource not specified. - Use "kubectl explain <resource>" for a detailed description of that resource (e.g. kubectl explain pods). + Use "kubectl explain " for a detailed description of that resource (e.g. kubectl explain pods). See 'kubectl get -h' for help and examples + exit status 1`) + slackInvalidCmd = strings.NewReplacer("<", "<", ">", ">").Replace(discordInvalidCmd) configMapLabels = map[string]string{ "test.botkube.io": "true", } @@ -193,7 +195,7 @@ func runBotTest(t *testing.T, // Discord bot needs a bit more time to connect to Discord API. time.Sleep(appCfg.Discord.MessageWaitTimeout) t.Log("Waiting for interactive help") - expMessage := interactive.NewHelpMessage(config.CommPlatformIntegration(botDriver.Type()), appCfg.ClusterName, []string{"botkube/helm"}).Build() + expMessage := interactive.NewHelpMessage(config.CommPlatformIntegration(botDriver.Type()), appCfg.ClusterName, []string{"botkube/helm", "botkube/kubectl"}).Build() expMessage.ReplaceBotNamePlaceholder(botDriver.BotName()) err = botDriver.WaitForInteractiveMessagePostedRecentlyEqual(botDriver.BotUserID(), botDriver.Channel().ID(), @@ -219,7 +221,7 @@ func runBotTest(t *testing.T, t.Run("Help", func(t *testing.T) { command := "help" - expectedMessage := interactive.NewHelpMessage(config.CommPlatformIntegration(botDriver.Type()), appCfg.ClusterName, []string{"botkube/helm"}).Build() + expectedMessage := interactive.NewHelpMessage(config.CommPlatformIntegration(botDriver.Type()), appCfg.ClusterName, []string{"botkube/helm", "botkube/kubectl"}).Build() expectedMessage.ReplaceBotNamePlaceholder(botDriver.BotName()) botDriver.PostMessageToBot(t, botDriver.Channel().Identifier(), command) err = botDriver.WaitForLastInteractiveMessagePostedEqual(botDriver.BotUserID(), @@ -482,7 +484,10 @@ func runBotTest(t *testing.T, t.Run("Get forbidden resource", func(t *testing.T) { command := "kubectl get role" - expectedBody := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'role' resources in the 'default' Namespace on cluster '%s'. Use 'list executors' to see allowed executors.", appCfg.ClusterName)) + expectedBody := codeBlock(heredoc.Docf(` + Error from server (Forbidden): roles.rbac.authorization.k8s.io is forbidden: User "kubectl-first-channel" cannot list resource "roles" in API group "rbac.authorization.k8s.io" in the namespace "default" + + exit status 1`)) expectedMessage := fmt.Sprintf("%s\n%s", cmdHeader(command), expectedBody) botDriver.PostMessageToBot(t, botDriver.Channel().Identifier(), command) @@ -512,7 +517,10 @@ func runBotTest(t *testing.T, t.Run("Specify forbidden namespace", func(t *testing.T) { command := "kubectl get po --namespace team-b" - expectedBody := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'po' resources in the 'team-b' Namespace on cluster '%s'. Use 'list executors' to see allowed executors.", appCfg.ClusterName)) + expectedBody := codeBlock(heredoc.Docf(` + Error from server (Forbidden): pods is forbidden: User "kubectl-first-channel" cannot list resource "pods" in API group "" in the namespace "team-b" + + exit status 1`)) expectedMessage := fmt.Sprintf("%s\n%s", cmdHeader(command), expectedBody) botDriver.PostMessageToBot(t, botDriver.Channel().Identifier(), command) @@ -521,32 +529,53 @@ func runBotTest(t *testing.T, }) t.Run("Based on other bindings", func(t *testing.T) { - t.Run("Wait for Deployment (the 2st binding)", func(t *testing.T) { + t.Run("Wait for Deployment", func(t *testing.T) { command := fmt.Sprintf("kubectl wait deployment -n %s %s --for condition=Available=True", appCfg.Deployment.Namespace, appCfg.Deployment.Name) - assertionFn := func(msg string) (bool, int, string) { - return strings.Contains(msg, heredoc.Doc(fmt.Sprintf("`%s` on `%s`", command, appCfg.ClusterName))) && - strings.Contains(msg, "deployment.apps/botkube condition met"), 0, "" - } + expectedBody := codeBlock(`The "wait" command is not supported by the Botkube kubectl plugin.`) + expectedMessage := fmt.Sprintf("%s\n%s", cmdHeader(command), expectedBody) botDriver.PostMessageToBot(t, botDriver.Channel().Identifier(), command) - err = botDriver.WaitForMessagePosted(botDriver.BotUserID(), botDriver.Channel().ID(), 1, assertionFn) + err = botDriver.WaitForLastMessageEqual(botDriver.BotUserID(), botDriver.Channel().ID(), expectedMessage) assert.NoError(t, err) }) - t.Run("Exec (the 3rd binding which is disabled)", func(t *testing.T) { - command := "kubectl exec" - expectedBody := codeBlock(fmt.Sprintf("Sorry, the kubectl 'exec' command cannot be executed in the 'default' Namespace on cluster '%s'. Use 'list executors' to see allowed executors.", appCfg.ClusterName)) + t.Run("Exec (the kubectl which is disabled)", func(t *testing.T) { + command := fmt.Sprintf("kubectl exec deploy/%s -n %s -- date", appCfg.Deployment.Name, appCfg.Deployment.Namespace) + expectedBody := codeBlock(heredoc.Docf(` + Defaulted container "botkube" out of: botkube, cfg-watcher + Error from server (Forbidden): pods "botkube-pod" is forbidden: User "kubectl-first-channel" cannot create resource "pods/exec" in API group "" in the namespace "botkube" + + exit status 1`)) expectedMessage := fmt.Sprintf("%s\n%s", cmdHeader(command), expectedBody) botDriver.PostMessageToBot(t, botDriver.Channel().Identifier(), command) - err = botDriver.WaitForLastMessageEqual(botDriver.BotUserID(), botDriver.Channel().ID(), expectedMessage) + + podName, err := regexp.Compile(`"botkube-.*-.*" is`) + assert.NoError(t, err) + + assertionFn := func(msg string) (bool, int, string) { + msg = podName.ReplaceAllString(msg, `"botkube-pod" is`) + msg = trimTrailingLine(msg) + if !strings.EqualFold(expectedMessage, msg) { + count := countMatchBlock(expectedMessage, msg) + msgDiff := diff(expectedMessage, msg) + return false, count, msgDiff + } + return true, 0, "" + } + err = botDriver.WaitForMessagePosted(botDriver.BotUserID(), botDriver.Channel().ID(), 1, assertionFn) assert.NoError(t, err) }) - t.Run("Get all Pods (the 4th binding) with alias", func(t *testing.T) { + t.Run("Get all Pods with alias", func(t *testing.T) { aliasedCommand := "kgp -A" expandedCommand := "kubectl get pods -A" - expectedBody := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'pods' resources for all Namespaces on cluster '%s'. Use 'list executors' to see allowed executors.", appCfg.ClusterName)) + + expectedBody := codeBlock(heredoc.Docf(` + Error from server (Forbidden): pods is forbidden: User "kubectl-first-channel" cannot list resource "pods" in API group "" at the cluster scope + + exit status 1`)) + expectedMessage := fmt.Sprintf("%s\n%s", cmdHeader(expandedCommand), expectedBody) botDriver.PostMessageToBot(t, botDriver.Channel().Identifier(), aliasedCommand) @@ -554,7 +583,7 @@ func runBotTest(t *testing.T, assert.NoError(t, err) }) - t.Run("Get all Deployments (the 4th binding) with alias", func(t *testing.T) { + t.Run("Get all Deployments with alias", func(t *testing.T) { aliasedCommand := "kgda" expandedCommand := "kubectl get deployments -A" assertionFn := func(msg string) (bool, int, string) { @@ -960,8 +989,7 @@ func runBotTest(t *testing.T, EXECUTOR ENABLED ALIASES botkube/echo@v1.0.1-devel true e botkube/helm true - botkube/kubectl false k, kc - kubectl true k, kc`)) + botkube/kubectl true k, kc`)) expectedMessage := fmt.Sprintf("%s\n%s", cmdHeader(command), expectedBody) botDriver.PostMessageToBot(t, botDriver.Channel().Identifier(), command) diff --git a/test/e2e/slack_driver_test.go b/test/e2e/slack_driver_test.go index 2917cd119..7911c781d 100644 --- a/test/e2e/slack_driver_test.go +++ b/test/e2e/slack_driver_test.go @@ -130,7 +130,7 @@ func (s *slackTester) InviteBotToChannel(t *testing.T, channelID string) { func (s *slackTester) WaitForMessagePostedRecentlyEqual(userID, channelID, expectedMsg string) error { return s.WaitForMessagePosted(userID, channelID, recentMessagesLimit, func(msg string) (bool, int, string) { - msg = s.trimNewLine(msg) + msg = trimTrailingLine(msg) if !strings.EqualFold(expectedMsg, msg) { count := countMatchBlock(expectedMsg, msg) msgDiff := diff(expectedMsg, msg) @@ -142,13 +142,13 @@ func (s *slackTester) WaitForMessagePostedRecentlyEqual(userID, channelID, expec func (s *slackTester) WaitForLastMessageContains(userID, channelID, expectedMsgSubstring string) error { return s.WaitForMessagePosted(userID, channelID, 1, func(msg string) (bool, int, string) { - return strings.Contains(s.trimNewLine(msg), expectedMsgSubstring), 0, "" + return strings.Contains(trimTrailingLine(msg), expectedMsgSubstring), 0, "" }) } func (s *slackTester) WaitForLastMessageEqual(userID, channelID, expectedMsg string) error { return s.WaitForMessagePosted(userID, channelID, 1, func(msg string) (bool, int, string) { - msg = s.trimNewLine(msg) + msg = trimTrailingLine(msg) msg = formatx.RemoveHyperlinks(msg) // normalize the message URLs if msg != expectedMsg { count := countMatchBlock(expectedMsg, msg) @@ -371,7 +371,7 @@ func (s *slackTester) createChannel(t *testing.T, prefix string) (*slack.Channel return channel, cleanupFn } -func (s *slackTester) trimNewLine(msg string) string { +func trimTrailingLine(msg string) string { // There is always a `\n` on Slack messages due to Markdown formatting. // That should be replaced for RTM return strings.TrimSuffix(msg, "\n") From bb0c839bb6baee1e749433a129166d80df32bc63 Mon Sep 17 00:00:00 2001 From: Mateusz Szostok Date: Wed, 29 Mar 2023 12:02:04 +0200 Subject: [PATCH 2/2] Apply suggestions after review --- .gitignore | 1 + CONTRIBUTING.md | 39 +- comm_config.yaml.tpl | 121 ------ global_config.yaml.tpl | 395 ------------------ helm/botkube/e2e-test-values.yaml | 218 +++++----- pkg/bot/interactive/help.go | 2 +- ...m_headers_and_default_new_lines.golden.txt | 2 +- ...m_new_lines_and_default_headers.golden.txt | 2 +- ...stInteractiveMessageToPlaintext.golden.txt | 2 +- pkg/execute/plugin_discovery.go | 4 +- 10 files changed, 144 insertions(+), 642 deletions(-) delete mode 100644 comm_config.yaml.tpl delete mode 100644 global_config.yaml.tpl diff --git a/.gitignore b/.gitignore index 8461feb7c..7f630b783 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ tags /analytics.yaml /resource_config.yaml /comm_config.yaml +/local_config.yaml /bin diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1f05af89a..1c5a3dcba 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -92,19 +92,36 @@ For faster development, you can also build and run Botkube outside K8s cluster. go build ./cmd/botkube/ ``` -2. Use templates to create configuration files: - - ```sh - cp global_config.yaml.tpl resource_config.yaml - cp comm_config.yaml.tpl comm_config.yaml +2. Create a local configuration file to override default values. For example, set communication credentials, specify cluster name, and disable analytics: + + ```yaml + cat < local_config.yaml + communications: + default-group: + socketSlack: + enabled: true + channels: + default: + name: random + appToken: "xapp-xxxx" + botToken: "xoxb-xxxx" + configWatcher: + enabled: false + settings: + clusterName: "labs" + analytics: + # -- If true, sending anonymous analytics is disabled. To learn what date we collect, + # see [Privacy Policy](https://botkube.io/privacy#privacy-policy). + disable: true + EOF ``` - Edit the newly created `resource_config.yaml` and `comm_config.yaml` files to configure resource and set communication credentials. - -3. Export the path to directory of `config.yaml` + To learn more about configuration, visit https://docs.botkube.io/configuration/. +3. Export paths to configuration files. The priority will be given to the last (right-most) file specified. + ```sh - export BOTKUBE_CONFIG_PATHS="$(pwd)/resource_config.yaml,$(pwd)/comm_config.yaml" + export BOTKUBE_CONFIG_PATHS="$(pwd)/helm/botkube/values.yaml,$(pwd)/local_config.yaml" ``` 4. Export the path to Kubeconfig: @@ -145,8 +162,8 @@ For faster development, you can also build and run Botkube outside K8s cluster. go run test/helpers/plugin_server.go ``` - > **Note** - > If Botkube runs inside the k3d cluster, export the `PLUGIN_SERVER_HOST=http://host.k3d.internal` environment variable. + > **Note** + > If Botkube runs inside the k3d cluster, export the `PLUGIN_SERVER_HOST=http://host.k3d.internal` environment variable. 2. Export Botkube plugins cache directory: diff --git a/comm_config.yaml.tpl b/comm_config.yaml.tpl deleted file mode 100644 index fe243d1bf..000000000 --- a/comm_config.yaml.tpl +++ /dev/null @@ -1,121 +0,0 @@ -# Map of enabled communication mediums. The `communications` property name is an alias for a given configuration. -# -# Format: communications.{alias} -communications: - 'default-group': - # Settings for Slack with Socket Mode - socketSlack: - enabled: false - channels: - 'alias': - name: 'SLACK_CHANNEL' - bindings: - executors: - - 'kubectl-read-only' - sources: - - 'k8s-events' - botToken: "" # SLACK_BOT_TOKEN - appToken: "" # SLACK_APP_TOKEN - notification: - type: short # Change notification type short/long you want to receive. Type is optional and default is short. - - # Settings for Mattermost - mattermost: - enabled: false - url: 'MATTERMOST_SERVER_URL' # URL where Mattermost is running. e.g https://example.com:9243 - token: 'MATTERMOST_TOKEN' # Personal Access token generated by Botkube user - team: 'MATTERMOST_TEAM' # Mattermost Team to configure with Botkube - botName: 'Botkube' # Bot name - channels: - 'alias': - name: 'MATTERMOST_CHANNEL' # Mattermost Channel for receiving Botkube alerts: - notification: - # -- If true, the notifications are not sent to the channel. They can be enabled with `@Botkube` command anytime. - disabled: false - bindings: - executors: - - kubectl-read-only - sources: - - k8s-events - notification: - type: short # Change notification type short/long you want to receive. Type is optional and default is short. - - # Settings for MS Teams - teams: - enabled: false - appID: 'APPLICATION_ID' - appPassword: 'APPLICATION_PASSWORD' - botName: 'Botkube' - notification: - type: short - port: 3978 - - # Settings for Discord - discord: - enabled: false - token: 'DISCORD_TOKEN' # Botkube Bot Token - botID: 'DISCORD_BOT_ID' # Botkube Application Client ID - channels: - 'alias': - id: 'DISCORD_CHANNEL_ID' # Discord Channel id for receiving Botkube alerts: - notification: - # -- If true, the notifications are not sent to the channel. They can be enabled with `@Botkube` command anytime. - disabled: false - bindings: - executors: - - kubectl-read-only - sources: - - k8s-events - notification: - type: short # Change notification type short/long you want to receive. Type is optional and default is short. - - - # Settings for ELS - elasticsearch: - enabled: false - awsSigning: - enabled: false # enable awsSigning using IAM for Elastisearch hosted on AWS, if true make sure AWS environment variables are set. Refer https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html - awsRegion: 'us-east-1' # AWS region where Elasticsearch is deployed - roleArn: '' # AWS IAM Role arn to assume for credentials, use this only if you dont want to use the EC2 instance role or not running on AWS instance - server: 'ELASTICSEARCH_ADDRESS' # e.g https://example.com:9243 - username: 'ELASTICSEARCH_USERNAME' # Basic Auth - password: 'ELASTICSEARCH_PASSWORD' - skipTLSVerify: false # toggle verification of TLS certificate of the Elastic nodes. Verification is skipped when option is true. Enable to connect to clusters with self-signed certs - # ELS index settings - indices: - 'alias': - name: botkube - type: botkube-event - shards: 1 - bindings: - sources: - - "k8s-events" - # executors - not allowed in this case, ES is "sink" only. - # Settings for Webhook - webhook: - enabled: false - url: 'WEBHOOK_URL' # e.g https://example.com:80 - bindings: - # -- Notification sources configuration for the webhook. - sources: - - k8s-events - - # Settings for deprecated Slack integration. - # DEPRECATED: Legacy Slack integration has been deprecated and removed from the Slack App Directory. - # Use `socketSlack` instead. Read more here: https://docs.botkube.io/installation/slack/ - # This object will be removed as a part of https://github.com/kubeshop/botkube/issues/865. - slack: - enabled: false - channels: - 'alias': - name: 'SLACK_CHANNEL' - notification: - disabled: false - bindings: - executors: - - 'kubectl-read-only' - sources: - - 'k8s-events' - token: "" # SLACK_API_TOKEN - notification: - type: short diff --git a/global_config.yaml.tpl b/global_config.yaml.tpl deleted file mode 100644 index c23cf147c..000000000 --- a/global_config.yaml.tpl +++ /dev/null @@ -1,395 +0,0 @@ -# Format: actions.{alias} -actions: - # kubectl based action. - 'show-created-resource': - # If true, enables the action. - enabled: false - - # Action display name posted in the channels bound to the same source bindings. - displayName: "Display created resource" - # A text value denoting the command run by this action, may contain even based templated values. - # The executor is inferred directly from the command, e.g. here we require a kubectl executor - command: "kubectl describe {{ .Event.TypeMeta.Kind | lower }}{{ if .Event.Namespace }} -n {{ .Event.Namespace }}{{ end }} {{ .Event.Name }}" - - # Bindings for a given action. - bindings: - # Sources of events that trigger a given action. - sources: - - k8s-create-events - # Executors configuration for a given automation. - executors: - - kubectl-read-only - 'show-logs-on-error': - # If true, enables the action. - enabled: false - - # Action display name posted in the channels bound to the same source bindings. - displayName: "Show logs on error" - # A text value denoting the command run by this action, may contain even based templated values. - # The executor is inferred directly from the command, e.g. here we require a kubectl executor - command: "kubectl logs {{ .Event.TypeMeta.Kind | lower }}/{{ .Event.Name }} -n {{ .Event.Namespace }}" - - # Bindings for a given action. - bindings: - # Sources of events that trigger a given action. - sources: - - k8s-err-with-logs-events - # Executors configuration for a given automation. - executors: - - kubectl-read-only - - -# Map of sources. Source contains configuration for Kubernetes events and sending recommendations. -# The property name under `sources` object is an alias for a given configuration. You can define multiple sources configuration with different names. -# Key name is used as a binding reference. -# See the `values.yaml` file for full object. -# -## Format: sources.{alias} -sources: - 'k8s-recommendation-events': - displayName: "Kubernetes Recommendations" - # Describes Kubernetes source configuration. - # See the `values.yaml` file for full object. - kubernetes: - # Describes configuration for various recommendation insights. - recommendations: - # Recommendations for Pod Kubernetes resource. - pod: - # If true, notifies about Pod containers that use `latest` tag for images. - noLatestImageTag: true - # If true, notifies about Pod resources created without labels. - labelsSet: true - # Recommendations for Ingress Kubernetes resource. - ingress: - # If true, notifies about Ingress resources with invalid backend service reference. - backendServiceValid: true - # If true, notifies about Ingress resources with invalid TLS secret reference. - tlsSecretValid: true - - 'k8s-all-events': - displayName: "Kubernetes Info" - # Describes Kubernetes source configuration. - # See the `values.yaml` file for full object. - kubernetes: - # Describes namespaces for every Kubernetes resources you want to watch or exclude. - # These namespaces are applied to every resource specified in the resources list. - # However, every specified resource can override this by using its own namespaces object. - namespaces: &k8s-events-namespaces - # Include contains a list of allowed Namespaces. - # It can also contain regex expressions: - # `- ".*"` - to specify all Namespaces. - include: - - ".*" - # Exclude contains a list of Namespaces to be ignored even if allowed by Include. - # It can also contain regex expressions: - # `- "test-.*"` - to specif all Namespaces with `test-` prefix. - # Exclude list is checked before the Include list. - # exclude: [] - - # Describes event constraints for Kubernetes resources. - # These constraints are applied for every resource specified in the `resources` list, unless they are overridden by the resource's own `events` object. - event: - # Lists all event types to be watched. - types: - - create - - delete - - error - # Optional list of exact values or regex patterns to filter events by event reason. - # Skipped, if both include/exclude lists are empty. - reason: - # Include contains a list of allowed values. It can also contain regex expressions. - include: [] - # Exclude contains a list of values to be ignored even if allowed by Include. It can also contain regex expressions. - # Exclude list is checked before the Include list. - exclude: [] - # Optional list of exact values or regex patterns to filter event by event message. Skipped, if both include/exclude lists are empty. - # If a given event has multiple messages, it is considered a match if any of the messages match the constraints. - message: - # Include contains a list of allowed values. It can also contain regex expressions. - include: [] - # Exclude contains a list of values to be ignored even if allowed by Include. It can also contain regex expressions. - # Exclude list is checked before the Include list. - exclude: [] - - # Filters Kubernetes resources to watch by annotations. Each resource needs to have all the specified annotations. - # Regex expressions are not supported. - annotations: {} - # Filters Kubernetes resources to watch by labels. Each resource needs to have all the specified labels. - # Regex expressions are not supported. - labels: {} - - # Describes the Kubernetes resources to watch. - # Resources are identified by its type in `{group}/{version}/{kind (plural)}` format. Examples: `apps/v1/deployments`, `v1/pods`. - # Each resource can override the namespaces and event configuration by using dedicated `event` and `namespaces` field. - # Also, each resource can specify its own `annotations`, `labels` and `name` regex. - # See the `values.yaml` file for full object. - resources: - - type: v1/pods - namespaces: # Overrides 'source'.kubernetes.namespaces - include: - - ".*" - exclude: [] - annotations: {} # Overrides 'source'.kubernetes.annotations - labels: {} # Overrides 'source'.kubernetes.labels - # Optional resource name constraints. - name: - # Include contains a list of allowed values. It can also contain regex expressions. - include: [] - # Exclude contains a list of values to be ignored even if allowed by Include. It can also contain regex expressions. - # Exclude list is checked before the Include list. - exclude: [] - event: - # Overrides 'source'.kubernetes.event.reason - reason: - include: [] - exclude: [] - # Overrides 'source'.kubernetes.event.message - message: - include: [] - exclude: [] - # Overrides 'source'.kubernetes.event.types - types: - - create - - - type: v1/services - - type: networking.k8s.io/v1/ingresses - - type: v1/nodes - - type: v1/namespaces - - type: v1/persistentvolumes - - type: v1/persistentvolumeclaims - - type: v1/configmaps - - type: rbac.authorization.k8s.io/v1/roles - - type: rbac.authorization.k8s.io/v1/rolebindings - - type: rbac.authorization.k8s.io/v1/clusterrolebindings - - type: rbac.authorization.k8s.io/v1/clusterroles - - type: apps/v1/daemonsets - event: # Overrides 'source'.kubernetes.event - types: - - create - - update - - delete - - error - updateSetting: - includeDiff: true - fields: - - spec.template.spec.containers[*].image - - status.numberReady - - type: batch/v1/jobs - event: # Overrides 'source'.kubernetes.event - types: - - create - - update - - delete - - error - updateSetting: - includeDiff: true - fields: - - spec.template.spec.containers[*].image - - status.conditions[*].type - - type: apps/v1/deployments - event: # Overrides 'source'.kubernetes.event - types: - - create - - update - - delete - - error - updateSetting: - includeDiff: true - fields: - - spec.template.spec.containers[*].image - - status.availableReplicas - - type: apps/v1/statefulsets - event: # Overrides 'source'.kubernetes.event - types: - - create - - update - - delete - - error - updateSetting: - includeDiff: true - fields: - - spec.template.spec.containers[*].image - - status.readyReplicas - ## Custom resource example - # - type: velero.io/v1/backups - # namespaces: - # include: - # - ".*" - # exclude: - # - - # event: - # types: - # - create - # - update - # - delete - # - error - # updateSetting: - # includeDiff: true - # fields: - # - status.phase - - 'k8s-err-events': - displayName: "Kubernetes Errors" - - # Describes Kubernetes source configuration. - # See the `values.yaml` file for full object. - kubernetes: - # Describes namespaces for every Kubernetes resources you want to watch or exclude. - # These namespaces are applied to every resource specified in the resources list. - # However, every specified resource can override this by using its own namespaces object. - namespaces: *k8s-events-namespaces - - # Describes event constraints for Kubernetes resources. - # These constraints are applied for every resource specified in the `resources` list, unless they are overridden by the resource's own `events` object. - event: - # Lists all event types to be watched. - types: - - error - - # Describes the Kubernetes resources you want to watch. - # See the `values.yaml` file for full object. - resources: - - type: v1/pods - - type: v1/services - - type: networking.k8s.io/v1/ingresses - - type: v1/nodes - - type: v1/namespaces - - type: v1/persistentvolumes - - type: v1/persistentvolumeclaims - - type: v1/configmaps - - type: rbac.authorization.k8s.io/v1/roles - - type: rbac.authorization.k8s.io/v1/rolebindings - - type: rbac.authorization.k8s.io/v1/clusterrolebindings - - type: rbac.authorization.k8s.io/v1/clusterroles - - type: apps/v1/deployments - - type: apps/v1/statefulsets - - type: apps/v1/daemonsets - - type: batch/v1/jobs - 'k8s-err-with-logs-events': - displayName: "Kubernetes Errors for resources with logs" - - # Describes Kubernetes source configuration. - # See the `values.yaml` file for full object. - kubernetes: - # Describes namespaces for every Kubernetes resources you want to watch or exclude. - # These namespaces are applied to every resource specified in the resources list. - # However, every specified resource can override this by using its own namespaces object. - namespaces: *k8s-events-namespaces - - # Describes event constraints for Kubernetes resources. - # These constraints are applied for every resource specified in the `resources` list, unless they are overridden by the resource's own `events` object. - event: - # Lists all event types to be watched. - types: - - error - - # Describes the Kubernetes resources you want to watch. - # See the `values.yaml` file for full object. - resources: - - type: v1/pods - - type: apps/v1/deployments - - type: apps/v1/statefulsets - - type: apps/v1/daemonsets - - type: batch/v1/jobs - # `apps/v1/replicasets` excluded on purpose - to not show logs twice for a given higher-level resource (e.g. Deployment) - - 'k8s-create-events': - displayName: "Kubernetes Resource Created Events" - - # Describes Kubernetes source configuration. - # See the `values.yaml` file for full object. - kubernetes: - # Describes namespaces for every Kubernetes resources you want to watch or exclude. - # These namespaces are applied to every resource specified in the resources list. - # However, every specified resource can override this by using its own namespaces object. - namespaces: *k8s-events-namespaces - - # Describes event constraints for Kubernetes resources. - # These constraints are applied for every resource specified in the `resources` list, unless they are overridden by the resource's own `events` object. - event: - # Lists all event types to be watched. - types: - - create - - # Describes the Kubernetes resources you want to watch. - # See the `values.yaml` file for full object. - resources: - - type: v1/pods - - type: v1/services - - type: networking.k8s.io/v1/ingresses - - type: v1/nodes - - type: v1/namespaces - - type: v1/configmaps - - type: apps/v1/deployments - - type: apps/v1/statefulsets - - type: apps/v1/daemonsets - - type: batch/v1/jobs - - 'prometheus': - ## Prometheus source configuration - ## Plugin name syntax: /[@]. If version is not provided, the latest version from repository is used. - botkube/prometheus: - # If true, enables `prometheus` source. - enabled: false - config: - # Prometheus endpoint without api version and resource. - url: "http://localhost:9090" - # If set as true, Prometheus source plugin will not send alerts that is created before plugin start time. - ignoreOldAlerts: true - # Only the alerts that have state provided in this config will be sent as notification. https://pkg.go.dev/github.com/prometheus/prometheus/rules#AlertState - alertStates: ["firing", "pending", "inactive"] - # Logging configuration - log: - # Log level - level: info - -# Filter settings for various sources. -# Currently, all filters are globally enabled or disabled. -# You can enable or disable filters with `@Botkube enable/disable filters` commands. -filters: - kubernetes: - # If true, enables support for `botkube.io/disable` resource annotation. - objectAnnotationChecker: true - # If true, filters out Node-related events that are not important. - nodeEventsChecker: true - -# Setting to support multiple clusters -settings: - # Cluster name to differentiate incoming messages - clusterName: not-configured - # Set true to enable config watcher - # 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. -# -# Format: executors.{alias} -executors: - 'kubectl-read-only': - # Kubectl executor configs - kubectl: - namespaces: - include: [".*"] - # Set true to enable kubectl commands execution - enabled: false - # List of allowed commands - commands: - # method which are allowed - verbs: [ "api-resources", "api-versions", "cluster-info", "describe", "diff", "explain", "get", "logs", "top", "auth" ] - # resource configuration which is allowed - resources: [ "deployments", "pods" , "namespaces", "daemonsets", "statefulsets", "storageclasses", "nodes" ] - # set Namespace to execute botkube kubectl commands by default - defaultNamespace: default - # Set true to enable commands execution from configured channel only - restrictAccess: false diff --git a/helm/botkube/e2e-test-values.yaml b/helm/botkube/e2e-test-values.yaml index 5ea6be115..2cccf535f 100644 --- a/helm/botkube/e2e-test-values.yaml +++ b/helm/botkube/e2e-test-values.yaml @@ -74,7 +74,7 @@ sources: group: type: Static static: - values: [*static-group-name] # "botkube-plugins-read-only" is the default + values: [ *static-group-name ] # "botkube-plugins-read-only" is the default user: type: Static static: @@ -195,40 +195,40 @@ executors: enabled: true context: rbac: - group: + user: type: Static static: - values: [ "kubectl-first-channel" ] + value: "kubectl-first-channel" 'kubectl-exec-cmd': botkube/kubectl: enabled: false context: rbac: - group: + user: type: Static static: # 'exec' verb perms on 'botkube' and 'default' namespaces - values: [ "kc-exec-only" ] + value: "kc-exec-only" 'kubectl-not-bound-to-any-channel': botkube/kubectl: enabled: true context: rbac: - group: + user: type: Static static: # deployments port-forward across all namespaces - values: [ "kubectl-first-channel" ] + value: "kubectl-first-channel" 'kubectl-with-svc-label-perms': botkube/kubectl: enabled: true context: rbac: - group: + user: type: Static static: # service labeling across all namespaces - values: [ "kc-label-svc-all" ] + value: "kc-label-svc-all" 'other-plugins': botkube/echo@v1.0.1-devel: @@ -303,115 +303,115 @@ extraEnv: extraObjects: -# Group 'kubectl-first-channel': permissions for kubectl for first channel -## namespace scoped permissions -- apiVersion: rbac.authorization.k8s.io/v1 - kind: ClusterRole - metadata: &kubectl-wait - name: kubectl-first-channel-namespaced-perms - labels: - app.kubernetes.io/instance: botkube-e2e-test - rules: - - apiGroups: [ "apps" ] - resources: [ "deployments" ] - verbs: [ "get","watch","list" ] - - apiGroups: [""] - resources: ["configmaps", "pods"] - verbs: ["get", "watch", "list"] -- apiVersion: rbac.authorization.k8s.io/v1 - kind: RoleBinding - metadata: - <<: *kubectl-wait - namespace: botkube - roleRef: &kubectl-wait-role - apiGroup: rbac.authorization.k8s.io + # Group 'kubectl-first-channel': permissions for kubectl for first channel + ## namespace scoped permissions + - apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole - name: kubectl-first-channel-namespaced-perms - subjects: &kubectl-first-channel-subject - - kind: Group - name: kubectl-first-channel + metadata: &kubectl-wait + name: kubectl-first-channel-namespaced-perms + labels: + app.kubernetes.io/instance: botkube-e2e-test + rules: + - apiGroups: [ "apps" ] + resources: [ "deployments" ] + verbs: [ "get","watch","list" ] + - apiGroups: [ "" ] + resources: [ "configmaps", "pods" ] + verbs: [ "get", "watch", "list" ] + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + <<: *kubectl-wait + namespace: botkube + roleRef: &kubectl-wait-role apiGroup: rbac.authorization.k8s.io -- apiVersion: rbac.authorization.k8s.io/v1 - kind: RoleBinding - metadata: - <<: *kubectl-wait - namespace: default - roleRef: *kubectl-wait-role - subjects: *kubectl-first-channel-subject + kind: ClusterRole + name: kubectl-first-channel-namespaced-perms + subjects: &kubectl-first-channel-subject + - kind: User + name: kubectl-first-channel + apiGroup: rbac.authorization.k8s.io + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + <<: *kubectl-wait + namespace: default + roleRef: *kubectl-wait-role + subjects: *kubectl-first-channel-subject -### cluster permissions -- apiVersion: rbac.authorization.k8s.io/v1 - kind: ClusterRole - metadata: &kubectl-deploy-all-meta - name: kc-first-channel-cluster-perms - labels: - app.kubernetes.io/instance: botkube-e2e-test - rules: - - apiGroups: [ "apps" ] - resources: [ "deployments" ] - verbs: [ "get", "list" ] -- apiVersion: rbac.authorization.k8s.io/v1 - kind: ClusterRoleBinding - metadata: *kubectl-deploy-all-meta - roleRef: - apiGroup: rbac.authorization.k8s.io + ### cluster permissions + - apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole - name: kc-first-channel-cluster-perms - subjects: *kubectl-first-channel-subject + metadata: &kubectl-deploy-all-meta + name: kc-first-channel-cluster-perms + labels: + app.kubernetes.io/instance: botkube-e2e-test + rules: + - apiGroups: [ "apps" ] + resources: [ "deployments" ] + verbs: [ "get", "list" ] + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: *kubectl-deploy-all-meta + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kc-first-channel-cluster-perms + subjects: *kubectl-first-channel-subject -# Group 'kc-exec-only' -## exec only for default and botkube namespaces: -- apiVersion: rbac.authorization.k8s.io/v1 - kind: ClusterRole - metadata: &kc-exec-only-meta - name: kc-exec-only - labels: - app.kubernetes.io/instance: botkube-e2e-test - rules: - - apiGroups: [""] - resources: ["pods/exec"] - verbs: ["create"] -- apiVersion: rbac.authorization.k8s.io/v1 - kind: RoleBinding - metadata: - <<: *kc-exec-only-meta - namespace: botkube - roleRef: &kc-exec-only-role - apiGroup: rbac.authorization.k8s.io + # Group 'kc-exec-only' + ## exec only for default and botkube namespaces: + - apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole - name: kc-exec-only - subjects: &kc-exec-only-subject - - kind: Group + metadata: &kc-exec-only-meta name: kc-exec-only + labels: + app.kubernetes.io/instance: botkube-e2e-test + rules: + - apiGroups: [ "" ] + resources: [ "pods/exec" ] + verbs: [ "create" ] + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + <<: *kc-exec-only-meta + namespace: botkube + roleRef: &kc-exec-only-role apiGroup: rbac.authorization.k8s.io -- apiVersion: rbac.authorization.k8s.io/v1 - kind: RoleBinding - metadata: - <<: *kc-exec-only-meta - namespace: default - roleRef: *kc-exec-only-role - subjects: *kc-exec-only-subject + kind: ClusterRole + name: kc-exec-only + subjects: &kc-exec-only-subject + - kind: User + name: kc-exec-only + apiGroup: rbac.authorization.k8s.io + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + <<: *kc-exec-only-meta + namespace: default + roleRef: *kc-exec-only-role + subjects: *kc-exec-only-subject -# Group 'kc-label-svc-all': -## namespace scoped permissions -- apiVersion: rbac.authorization.k8s.io/v1 - kind: ClusterRole - metadata: &kc-label-svc-all-meta - name: kc-label-svc-all - labels: - app.kubernetes.io/instance: botkube-e2e-test - rules: - - apiGroups: [ "" ] - resources: [ "services" ] - verbs: [ "get", "patch" ] -- apiVersion: rbac.authorization.k8s.io/v1 - kind: ClusterRoleBinding - metadata: *kc-label-svc-all-meta - roleRef: - apiGroup: rbac.authorization.k8s.io + # Group 'kc-label-svc-all': + ## namespace scoped permissions + - apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole - name: kc-label-svc-all - subjects: - - kind: Group + metadata: &kc-label-svc-all-meta name: kc-label-svc-all + labels: + app.kubernetes.io/instance: botkube-e2e-test + rules: + - apiGroups: [ "" ] + resources: [ "services" ] + verbs: [ "get", "patch" ] + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: *kc-label-svc-all-meta + roleRef: apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kc-label-svc-all + subjects: + - kind: User + name: kc-label-svc-all + apiGroup: rbac.authorization.k8s.io diff --git a/pkg/bot/interactive/help.go b/pkg/bot/interactive/help.go index f28dd420d..2679cb1bc 100644 --- a/pkg/bot/interactive/help.go +++ b/pkg/bot/interactive/help.go @@ -176,7 +176,7 @@ func (h *HelpMessage) executorSections() []api.Section { return []api.Section{ { Base: api.Base{ - Header: "Manage executors", + Header: "List executors and aliases", }, Buttons: []api.Button{ h.btnBuilder.ForCommandWithDescCmd("List executors", "list executors"), diff --git a/pkg/bot/interactive/testdata/TestInteractiveMessageToMarkdown/render_with_custom_headers_and_default_new_lines.golden.txt b/pkg/bot/interactive/testdata/TestInteractiveMessageToMarkdown/render_with_custom_headers_and_default_new_lines.golden.txt index 58a19ac15..a6103d621 100644 --- a/pkg/bot/interactive/testdata/TestInteractiveMessageToMarkdown/render_with_custom_headers_and_default_new_lines.golden.txt +++ b/pkg/bot/interactive/testdata/TestInteractiveMessageToMarkdown/render_with_custom_headers_and_default_new_lines.golden.txt @@ -28,7 +28,7 @@ By default, Botkube will notify only about cluster errors and recommendations. ``` • `@Botkube show config` -*Manage executors* +*List executors and aliases* • `@Botkube list executors` • `@Botkube list aliases` diff --git a/pkg/bot/interactive/testdata/TestInteractiveMessageToMarkdown/render_with_custom_new_lines_and_default_headers.golden.txt b/pkg/bot/interactive/testdata/TestInteractiveMessageToMarkdown/render_with_custom_new_lines_and_default_headers.golden.txt index 534afb117..0137f22ba 100644 --- a/pkg/bot/interactive/testdata/TestInteractiveMessageToMarkdown/render_with_custom_new_lines_and_default_headers.golden.txt +++ b/pkg/bot/interactive/testdata/TestInteractiveMessageToMarkdown/render_with_custom_new_lines_and_default_headers.golden.txt @@ -4,4 +4,4 @@ Botkube is now active for "testing" cluster :rocket:

**Ping your cluster* @Botkube [list|enable|disable] action [action name] ```
• `@Botkube list actions`

**View current Botkube configuration**
``` @Botkube show config -```
• `@Botkube show config`

**Manage executors**
• `@Botkube list executors`
• `@Botkube list aliases`

**Run kubectl commands (if enabled)**
You can run kubectl commands directly from Platform!
• `@Botkube kubectl get services`
• `@Botkube kubectl get pods`
• `@Botkube kubectl get deployments`

**Angry? Amazed?**
Give feedback: https://feedback.botkube.io

Read our docs: https://docs.botkube.io
Join our Slack: https://join.botkube.io
Follow us on Twitter: https://twitter.com/botkube_io
\ No newline at end of file +```
• `@Botkube show config`

**List executors and aliases**
• `@Botkube list executors`
• `@Botkube list aliases`

**Run kubectl commands (if enabled)**
You can run kubectl commands directly from Platform!
• `@Botkube kubectl get services`
• `@Botkube kubectl get pods`
• `@Botkube kubectl get deployments`

**Angry? Amazed?**
Give feedback: https://feedback.botkube.io

Read our docs: https://docs.botkube.io
Join our Slack: https://join.botkube.io
Follow us on Twitter: https://twitter.com/botkube_io
\ No newline at end of file diff --git a/pkg/bot/interactive/testdata/TestInteractiveMessageToPlaintext.golden.txt b/pkg/bot/interactive/testdata/TestInteractiveMessageToPlaintext.golden.txt index dfd79ee68..ab53c7e26 100644 --- a/pkg/bot/interactive/testdata/TestInteractiveMessageToPlaintext.golden.txt +++ b/pkg/bot/interactive/testdata/TestInteractiveMessageToPlaintext.golden.txt @@ -23,7 +23,7 @@ View current Botkube configuration • @Botkube show config -Manage executors +List executors and aliases • @Botkube list executors • @Botkube list aliases diff --git a/pkg/execute/plugin_discovery.go b/pkg/execute/plugin_discovery.go index 8f52163c3..2bbd2ea7e 100644 --- a/pkg/execute/plugin_discovery.go +++ b/pkg/execute/plugin_discovery.go @@ -1,8 +1,8 @@ package execute var staticPluginDiscovery = map[string]string{ - "kubectl": "No `kubectl` commands are enabled in this channel. To learn how to enable them, visit https://docs.botkube.io/configuration/executor/kubectl", - "helm": "No `helm` commands are enabled in this channel. To learn how to enable them, visit https://docs.botkube.io/configuration/executor/helm", + "kubectl": "`kubectl` commands are disabled for this channel. To learn how to enable Kubectl executor, visit https://docs.botkube.io/configuration/executor/kubectl", + "helm": "`helm` commands are disabled for this channel. To learn how to enable Helm executor, visit https://docs.botkube.io/configuration/executor/helm", } // GetInstallHelpForKnownPlugin returns install help for a known plugin.