diff --git a/build/Dockerfile b/build/Dockerfile index def64a0886..8db8b15e9c 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 a9e7086cd0..1619d1c931 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 f8e062f235..6bb704cad4 100644 --- a/helm/botkube/README.md +++ b/helm/botkube/README.md @@ -46,208 +46,201 @@ Controller for the Botkube Slack app which helps you monitor your Kubernetes clu | [actions.show-logs-on-error.bindings.executors](./values.yaml#L104) | list | `["k8s-default-tools"]` | Executors configuration used to execute a configured command. | | [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. | -| [executors.k8s-default-tools.botkube/helm.context.rbac](./values.yaml#L122) | object | `{"group":{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"},"user":{"prefix":"","static":{"value":"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"},"user":{"prefix":"","static":{"value":"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"},"user":{"prefix":"","static":{"value":"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"},"user":{"prefix":"","static":{"value":"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"},"user":{"prefix":"","static":{"value":"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"},"user":{"prefix":"","static":{"value":"botkube-plugins-default"},"type":"Static"}}` | RBAC configuration for this plugin. | -| [sources.k8s-recommendation-events.botkube/kubernetes.context.rbac](./values.yaml#L122) | object | `{"group":{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"},"user":{"prefix":"","static":{"value":"botkube-plugins-default"},"type":"Static"}}` | RBAC configuration for this plugin. | -| [sources.k8s-err-with-logs-events.botkube/kubernetes.context.rbac.group](./values.yaml#L124) | object | `{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"}` | Static impersonation for a given username and groups. | -| [executors.k8s-default-tools.botkube/helm.context.rbac.group](./values.yaml#L124) | object | `{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"}` | Static impersonation for a given username and groups. | -| [sources.k8s-create-events.botkube/kubernetes.context.rbac.group](./values.yaml#L124) | object | `{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"}` | Static impersonation for a given username and groups. | -| [sources.k8s-err-events.botkube/kubernetes.context.rbac.group](./values.yaml#L124) | object | `{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"}` | Static impersonation for a given username and groups. | -| [sources.k8s-all-events.botkube/kubernetes.context.rbac.group](./values.yaml#L124) | object | `{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"}` | Static impersonation for a given username and groups. | -| [sources.k8s-recommendation-events.botkube/kubernetes.context.rbac.group](./values.yaml#L124) | object | `{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"}` | Static impersonation for a given username and groups. | -| [executors.k8s-default-tools.botkube/kubectl.context.rbac.group](./values.yaml#L124) | object | `{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"}` | Static impersonation for a given username and groups. | -| [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-all-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-err-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/helm.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.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. | -| [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-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. | -| [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-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. | -| [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-err-events.botkube/kubernetes.context.rbac.user.prefix](./values.yaml#L134) | string | `""` | Prefix that will be applied to .static.value[*]. | -| [sources.k8s-all-events.botkube/kubernetes.context.rbac.user.prefix](./values.yaml#L134) | string | `""` | Prefix that will be applied to .static.value[*]. | -| [executors.k8s-default-tools.botkube/helm.context.rbac.user.prefix](./values.yaml#L134) | string | `""` | Prefix that will be applied to .static.value[*]. | -| [sources.k8s-err-with-logs-events.botkube/kubernetes.context.rbac.user.prefix](./values.yaml#L134) | string | `""` | Prefix that will be applied to .static.value[*]. | -| [executors.k8s-default-tools.botkube/kubectl.context.rbac.user.prefix](./values.yaml#L134) | string | `""` | Prefix that will be applied to .static.value[*]. | -| [sources.k8s-recommendation-events.botkube/kubernetes.context.rbac.user.prefix](./values.yaml#L134) | string | `""` | Prefix that will be applied to .static.value[*]. | -| [sources.k8s-create-events.botkube/kubernetes.context.rbac.user.prefix](./values.yaml#L134) | string | `""` | Prefix that will be applied to .static.value[*]. | -| [sources.k8s-all-events.botkube/kubernetes.context.rbac.user.static.value](./values.yaml#L137) | string | `"botkube-plugins-default"` | Name of user.rbac.authorization.k8s.io the plugin will be bound to. | -| [executors.k8s-default-tools.botkube/kubectl.context.rbac.user.static.value](./values.yaml#L137) | string | `"botkube-plugins-default"` | Name of user.rbac.authorization.k8s.io the plugin will be bound to. | -| [sources.k8s-err-with-logs-events.botkube/kubernetes.context.rbac.user.static.value](./values.yaml#L137) | string | `"botkube-plugins-default"` | Name of user.rbac.authorization.k8s.io the plugin will be bound to. | -| [sources.k8s-create-events.botkube/kubernetes.context.rbac.user.static.value](./values.yaml#L137) | string | `"botkube-plugins-default"` | Name of user.rbac.authorization.k8s.io the plugin will be bound to. | -| [executors.k8s-default-tools.botkube/helm.context.rbac.user.static.value](./values.yaml#L137) | string | `"botkube-plugins-default"` | Name of user.rbac.authorization.k8s.io the plugin will be bound to. | -| [sources.k8s-err-events.botkube/kubernetes.context.rbac.user.static.value](./values.yaml#L137) | string | `"botkube-plugins-default"` | Name of user.rbac.authorization.k8s.io the plugin will be bound to. | -| [sources.k8s-recommendation-events.botkube/kubernetes.context.rbac.user.static.value](./values.yaml#L137) | string | `"botkube-plugins-default"` | Name of user.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. | -| [sources.k8s-recommendation-events.botkube/kubernetes.config.recommendations.pod](./values.yaml#L146) | object | `{"labelsSet":true,"noLatestImageTag":true}` | Recommendations for Pod Kubernetes resource. | -| [sources.k8s-recommendation-events.botkube/kubernetes.config.recommendations.pod.noLatestImageTag](./values.yaml#L148) | bool | `true` | If true, notifies about Pod containers that use `latest` tag for images. | -| [sources.k8s-recommendation-events.botkube/kubernetes.config.recommendations.pod.labelsSet](./values.yaml#L150) | bool | `true` | If true, notifies about Pod resources created without labels. | -| [sources.k8s-recommendation-events.botkube/kubernetes.config.recommendations.ingress](./values.yaml#L152) | object | `{"backendServiceValid":true,"tlsSecretValid":true}` | Recommendations for Ingress Kubernetes resource. | -| [sources.k8s-recommendation-events.botkube/kubernetes.config.recommendations.ingress.backendServiceValid](./values.yaml#L154) | bool | `true` | If true, notifies about Ingress resources with invalid backend service reference. | -| [sources.k8s-recommendation-events.botkube/kubernetes.config.recommendations.ingress.tlsSecretValid](./values.yaml#L156) | bool | `true` | If true, notifies about Ingress resources with invalid TLS secret reference. | -| [sources.k8s-all-events.botkube/kubernetes](./values.yaml#L162) | object | See the `values.yaml` file for full object. | Describes Kubernetes source configuration. | -| [sources.k8s-all-events.botkube/kubernetes.config.filters](./values.yaml#L168) | object | See the `values.yaml` file for full object. | Filter settings for various sources. | -| [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-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-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-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.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. | -| [sources.k8s-all-events.botkube/kubernetes.config.event.reason](./values.yaml#L198) | object | `{"exclude":[],"include":[]}` | Optional list of exact values or regex patterns to filter events by event reason. Skipped, if both include/exclude lists are empty. | -| [sources.k8s-all-events.botkube/kubernetes.config.event.reason.include](./values.yaml#L200) | list | `[]` | Include contains a list of allowed values. It can also contain regex expressions. | -| [sources.k8s-all-events.botkube/kubernetes.config.event.reason.exclude](./values.yaml#L203) | list | `[]` | 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. | -| [sources.k8s-all-events.botkube/kubernetes.config.event.message](./values.yaml#L206) | object | `{"exclude":[],"include":[]}` | 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. | -| [sources.k8s-all-events.botkube/kubernetes.config.event.message.include](./values.yaml#L208) | list | `[]` | Include contains a list of allowed values. It can also contain regex expressions. | -| [sources.k8s-all-events.botkube/kubernetes.config.event.message.exclude](./values.yaml#L211) | list | `[]` | 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. | -| [sources.k8s-all-events.botkube/kubernetes.config.annotations](./values.yaml#L215) | object | `{}` | Filters Kubernetes resources to watch by annotations. Each resource needs to have all the specified annotations. Regex expressions are not supported. | -| [sources.k8s-all-events.botkube/kubernetes.config.labels](./values.yaml#L218) | object | `{}` | Filters Kubernetes resources to watch by labels. Each resource needs to have all the specified labels. Regex expressions are not supported. | -| [sources.k8s-all-events.botkube/kubernetes.config.resources](./values.yaml#L225) | list | See the `values.yaml` file for full object. | 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. | -| [sources.k8s-err-events.botkube/kubernetes](./values.yaml#L335) | object | See the `values.yaml` file for full object. | Describes Kubernetes source configuration. | -| [sources.k8s-err-events.botkube/kubernetes.config.namespaces](./values.yaml#L342) | 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-events.botkube/kubernetes.config.event](./values.yaml#L346) | object | `{"types":["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-err-events.botkube/kubernetes.config.event.types](./values.yaml#L348) | list | `["error"]` | Lists all event types to be watched. | -| [sources.k8s-err-events.botkube/kubernetes.config.resources](./values.yaml#L353) | list | See the `values.yaml` file for full object. | Describes the Kubernetes resources you want to watch. | -| [sources.k8s-err-with-logs-events.botkube/kubernetes](./values.yaml#L375) | object | See the `values.yaml` file for full object. | Describes Kubernetes source configuration. | -| [sources.k8s-err-with-logs-events.botkube/kubernetes.config.namespaces](./values.yaml#L382) | 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.event](./values.yaml#L386) | object | `{"types":["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-err-with-logs-events.botkube/kubernetes.config.event.types](./values.yaml#L388) | list | `["error"]` | Lists all event types to be watched. | -| [sources.k8s-err-with-logs-events.botkube/kubernetes.config.resources](./values.yaml#L393) | list | See the `values.yaml` file for full object. | Describes the Kubernetes resources you want to watch. | -| [sources.k8s-create-events.botkube/kubernetes](./values.yaml#L406) | object | See the `values.yaml` file for full object. | Describes Kubernetes source configuration. | -| [sources.k8s-create-events.botkube/kubernetes.config.namespaces](./values.yaml#L413) | 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-create-events.botkube/kubernetes.config.event](./values.yaml#L417) | object | `{"types":["create"]}` | 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-create-events.botkube/kubernetes.config.event.types](./values.yaml#L419) | list | `["create"]` | Lists all event types to be watched. | -| [sources.k8s-create-events.botkube/kubernetes.config.resources](./values.yaml#L424) | list | See the `values.yaml` file for full object. | Describes the Kubernetes resources you want to watch. | -| [sources.prometheus.botkube/prometheus.enabled](./values.yaml#L441) | bool | `false` | If true, enables `prometheus` source. | -| [sources.prometheus.botkube/prometheus.config.url](./values.yaml#L444) | string | `"http://localhost:9090"` | Prometheus endpoint without api version and resource. | -| [sources.prometheus.botkube/prometheus.config.ignoreOldAlerts](./values.yaml#L446) | bool | `true` | If set as true, Prometheus source plugin will not send alerts that is created before plugin start time. | -| [sources.prometheus.botkube/prometheus.config.alertStates](./values.yaml#L448) | list | `["firing","pending","inactive"]` | 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 | -| [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.context.rbac](./values.yaml#L121) | object | `{"group":{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"},"user":{"prefix":"","static":{"value":"botkube-plugins-default"},"type":"Static"}}` | RBAC configuration for this plugin. | +| [sources.k8s-recommendation-events.botkube/kubernetes.context.rbac](./values.yaml#L121) | object | `{"group":{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"},"user":{"prefix":"","static":{"value":"botkube-plugins-default"},"type":"Static"}}` | RBAC configuration for this plugin. | +| [sources.k8s-err-with-logs-events.botkube/kubernetes.context.rbac](./values.yaml#L121) | object | `{"group":{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"},"user":{"prefix":"","static":{"value":"botkube-plugins-default"},"type":"Static"}}` | RBAC configuration for this plugin. | +| [sources.k8s-all-events.botkube/kubernetes.context.rbac](./values.yaml#L121) | object | `{"group":{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"},"user":{"prefix":"","static":{"value":"botkube-plugins-default"},"type":"Static"}}` | RBAC configuration for this plugin. | +| [executors.k8s-default-tools.botkube/kubectl.context.rbac](./values.yaml#L121) | object | `{"group":{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"},"user":{"prefix":"","static":{"value":"botkube-plugins-default"},"type":"Static"}}` | RBAC configuration for this plugin. | +| [sources.k8s-err-events.botkube/kubernetes.context.rbac](./values.yaml#L121) | object | `{"group":{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"},"user":{"prefix":"","static":{"value":"botkube-plugins-default"},"type":"Static"}}` | RBAC configuration for this plugin. | +| [sources.k8s-create-events.botkube/kubernetes.context.rbac](./values.yaml#L121) | object | `{"group":{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"},"user":{"prefix":"","static":{"value":"botkube-plugins-default"},"type":"Static"}}` | RBAC configuration for this plugin. | +| [sources.k8s-err-with-logs-events.botkube/kubernetes.context.rbac.group](./values.yaml#L123) | object | `{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"}` | Static impersonation for a given username and groups. | +| [sources.k8s-create-events.botkube/kubernetes.context.rbac.group](./values.yaml#L123) | object | `{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"}` | Static impersonation for a given username and groups. | +| [sources.k8s-all-events.botkube/kubernetes.context.rbac.group](./values.yaml#L123) | object | `{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"}` | Static impersonation for a given username and groups. | +| [executors.k8s-default-tools.botkube/helm.context.rbac.group](./values.yaml#L123) | object | `{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"}` | Static impersonation for a given username and groups. | +| [executors.k8s-default-tools.botkube/kubectl.context.rbac.group](./values.yaml#L123) | object | `{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"}` | Static impersonation for a given username and groups. | +| [sources.k8s-recommendation-events.botkube/kubernetes.context.rbac.group](./values.yaml#L123) | object | `{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"}` | Static impersonation for a given username and groups. | +| [sources.k8s-err-events.botkube/kubernetes.context.rbac.group](./values.yaml#L123) | object | `{"prefix":"","static":{"values":["botkube-plugins-default"]},"type":"Static"}` | Static impersonation for a given username and groups. | +| [executors.k8s-default-tools.botkube/helm.context.rbac.group.prefix](./values.yaml#L126) | string | `""` | Prefix that will be applied to .static.value[*]. | +| [executors.k8s-default-tools.botkube/kubectl.context.rbac.group.prefix](./values.yaml#L126) | string | `""` | Prefix that will be applied to .static.value[*]. | +| [sources.k8s-all-events.botkube/kubernetes.context.rbac.group.prefix](./values.yaml#L126) | string | `""` | Prefix that will be applied to .static.value[*]. | +| [sources.k8s-recommendation-events.botkube/kubernetes.context.rbac.group.prefix](./values.yaml#L126) | string | `""` | Prefix that will be applied to .static.value[*]. | +| [sources.k8s-err-events.botkube/kubernetes.context.rbac.group.prefix](./values.yaml#L126) | string | `""` | Prefix that will be applied to .static.value[*]. | +| [sources.k8s-err-with-logs-events.botkube/kubernetes.context.rbac.group.prefix](./values.yaml#L126) | string | `""` | Prefix that will be applied to .static.value[*]. | +| [sources.k8s-create-events.botkube/kubernetes.context.rbac.group.prefix](./values.yaml#L126) | string | `""` | Prefix that will be applied to .static.value[*]. | +| [sources.k8s-err-with-logs-events.botkube/kubernetes.context.rbac.group.static.values](./values.yaml#L129) | 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#L129) | 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#L129) | 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#L129) | 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#L129) | 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#L129) | 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#L129) | 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.user.prefix](./values.yaml#L133) | string | `""` | Prefix that will be applied to .static.value[*]. | +| [sources.k8s-all-events.botkube/kubernetes.context.rbac.user.prefix](./values.yaml#L133) | string | `""` | Prefix that will be applied to .static.value[*]. | +| [sources.k8s-create-events.botkube/kubernetes.context.rbac.user.prefix](./values.yaml#L133) | string | `""` | Prefix that will be applied to .static.value[*]. | +| [sources.k8s-recommendation-events.botkube/kubernetes.context.rbac.user.prefix](./values.yaml#L133) | string | `""` | Prefix that will be applied to .static.value[*]. | +| [executors.k8s-default-tools.botkube/helm.context.rbac.user.prefix](./values.yaml#L133) | string | `""` | Prefix that will be applied to .static.value[*]. | +| [executors.k8s-default-tools.botkube/kubectl.context.rbac.user.prefix](./values.yaml#L133) | string | `""` | Prefix that will be applied to .static.value[*]. | +| [sources.k8s-err-with-logs-events.botkube/kubernetes.context.rbac.user.prefix](./values.yaml#L133) | string | `""` | Prefix that will be applied to .static.value[*]. | +| [sources.k8s-err-with-logs-events.botkube/kubernetes.context.rbac.user.static.value](./values.yaml#L136) | string | `"botkube-plugins-default"` | Name of user.rbac.authorization.k8s.io the plugin will be bound to. | +| [executors.k8s-default-tools.botkube/kubectl.context.rbac.user.static.value](./values.yaml#L136) | string | `"botkube-plugins-default"` | Name of user.rbac.authorization.k8s.io the plugin will be bound to. | +| [sources.k8s-create-events.botkube/kubernetes.context.rbac.user.static.value](./values.yaml#L136) | string | `"botkube-plugins-default"` | Name of user.rbac.authorization.k8s.io the plugin will be bound to. | +| [sources.k8s-all-events.botkube/kubernetes.context.rbac.user.static.value](./values.yaml#L136) | string | `"botkube-plugins-default"` | Name of user.rbac.authorization.k8s.io the plugin will be bound to. | +| [sources.k8s-recommendation-events.botkube/kubernetes.context.rbac.user.static.value](./values.yaml#L136) | string | `"botkube-plugins-default"` | Name of user.rbac.authorization.k8s.io the plugin will be bound to. | +| [executors.k8s-default-tools.botkube/helm.context.rbac.user.static.value](./values.yaml#L136) | string | `"botkube-plugins-default"` | Name of user.rbac.authorization.k8s.io the plugin will be bound to. | +| [sources.k8s-err-events.botkube/kubernetes.context.rbac.user.static.value](./values.yaml#L136) | string | `"botkube-plugins-default"` | Name of user.rbac.authorization.k8s.io the plugin will be bound to. | +| [sources.k8s-recommendation-events.botkube/kubernetes.config.recommendations](./values.yaml#L143) | object | `{"ingress":{"backendServiceValid":true,"tlsSecretValid":true},"pod":{"labelsSet":true,"noLatestImageTag":true}}` | Describes configuration for various recommendation insights. | +| [sources.k8s-recommendation-events.botkube/kubernetes.config.recommendations.pod](./values.yaml#L145) | object | `{"labelsSet":true,"noLatestImageTag":true}` | Recommendations for Pod Kubernetes resource. | +| [sources.k8s-recommendation-events.botkube/kubernetes.config.recommendations.pod.noLatestImageTag](./values.yaml#L147) | bool | `true` | If true, notifies about Pod containers that use `latest` tag for images. | +| [sources.k8s-recommendation-events.botkube/kubernetes.config.recommendations.pod.labelsSet](./values.yaml#L149) | bool | `true` | If true, notifies about Pod resources created without labels. | +| [sources.k8s-recommendation-events.botkube/kubernetes.config.recommendations.ingress](./values.yaml#L151) | object | `{"backendServiceValid":true,"tlsSecretValid":true}` | Recommendations for Ingress Kubernetes resource. | +| [sources.k8s-recommendation-events.botkube/kubernetes.config.recommendations.ingress.backendServiceValid](./values.yaml#L153) | bool | `true` | If true, notifies about Ingress resources with invalid backend service reference. | +| [sources.k8s-recommendation-events.botkube/kubernetes.config.recommendations.ingress.tlsSecretValid](./values.yaml#L155) | bool | `true` | If true, notifies about Ingress resources with invalid TLS secret reference. | +| [sources.k8s-all-events.botkube/kubernetes](./values.yaml#L161) | object | See the `values.yaml` file for full object. | Describes Kubernetes source configuration. | +| [sources.k8s-all-events.botkube/kubernetes.config.filters](./values.yaml#L167) | object | See the `values.yaml` file for full object. | Filter settings for various sources. | +| [sources.k8s-all-events.botkube/kubernetes.config.filters.objectAnnotationChecker](./values.yaml#L169) | bool | `true` | If true, enables support for `botkube.io/disable` resource annotation. | +| [sources.k8s-all-events.botkube/kubernetes.config.filters.nodeEventsChecker](./values.yaml#L171) | bool | `true` | If true, filters out Node-related events that are not important. | +| [sources.k8s-all-events.botkube/kubernetes.config.namespaces](./values.yaml#L175) | 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-events.botkube/kubernetes.config.namespaces.include](./values.yaml#L179) | 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#L179) | 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#L179) | 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#L179) | 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#L189) | 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#L191) | list | `["create","delete","error"]` | Lists all event types to be watched. | +| [sources.k8s-all-events.botkube/kubernetes.config.event.reason](./values.yaml#L197) | object | `{"exclude":[],"include":[]}` | Optional list of exact values or regex patterns to filter events by event reason. Skipped, if both include/exclude lists are empty. | +| [sources.k8s-all-events.botkube/kubernetes.config.event.reason.include](./values.yaml#L199) | list | `[]` | Include contains a list of allowed values. It can also contain regex expressions. | +| [sources.k8s-all-events.botkube/kubernetes.config.event.reason.exclude](./values.yaml#L202) | list | `[]` | 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. | +| [sources.k8s-all-events.botkube/kubernetes.config.event.message](./values.yaml#L205) | object | `{"exclude":[],"include":[]}` | 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. | +| [sources.k8s-all-events.botkube/kubernetes.config.event.message.include](./values.yaml#L207) | list | `[]` | Include contains a list of allowed values. It can also contain regex expressions. | +| [sources.k8s-all-events.botkube/kubernetes.config.event.message.exclude](./values.yaml#L210) | list | `[]` | 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. | +| [sources.k8s-all-events.botkube/kubernetes.config.annotations](./values.yaml#L214) | object | `{}` | Filters Kubernetes resources to watch by annotations. Each resource needs to have all the specified annotations. Regex expressions are not supported. | +| [sources.k8s-all-events.botkube/kubernetes.config.labels](./values.yaml#L217) | object | `{}` | Filters Kubernetes resources to watch by labels. Each resource needs to have all the specified labels. Regex expressions are not supported. | +| [sources.k8s-all-events.botkube/kubernetes.config.resources](./values.yaml#L224) | list | See the `values.yaml` file for full object. | 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. | +| [sources.k8s-err-events.botkube/kubernetes](./values.yaml#L334) | object | See the `values.yaml` file for full object. | Describes Kubernetes source configuration. | +| [sources.k8s-err-events.botkube/kubernetes.config.namespaces](./values.yaml#L341) | 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-events.botkube/kubernetes.config.event](./values.yaml#L345) | object | `{"types":["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-err-events.botkube/kubernetes.config.event.types](./values.yaml#L347) | list | `["error"]` | Lists all event types to be watched. | +| [sources.k8s-err-events.botkube/kubernetes.config.resources](./values.yaml#L352) | list | See the `values.yaml` file for full object. | Describes the Kubernetes resources you want to watch. | +| [sources.k8s-err-with-logs-events.botkube/kubernetes](./values.yaml#L374) | object | See the `values.yaml` file for full object. | Describes Kubernetes source configuration. | +| [sources.k8s-err-with-logs-events.botkube/kubernetes.config.namespaces](./values.yaml#L381) | 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.event](./values.yaml#L385) | object | `{"types":["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-err-with-logs-events.botkube/kubernetes.config.event.types](./values.yaml#L387) | list | `["error"]` | Lists all event types to be watched. | +| [sources.k8s-err-with-logs-events.botkube/kubernetes.config.resources](./values.yaml#L392) | list | See the `values.yaml` file for full object. | Describes the Kubernetes resources you want to watch. | +| [sources.k8s-create-events.botkube/kubernetes](./values.yaml#L405) | object | See the `values.yaml` file for full object. | Describes Kubernetes source configuration. | +| [sources.k8s-create-events.botkube/kubernetes.config.namespaces](./values.yaml#L412) | 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-create-events.botkube/kubernetes.config.event](./values.yaml#L416) | object | `{"types":["create"]}` | 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-create-events.botkube/kubernetes.config.event.types](./values.yaml#L418) | list | `["create"]` | Lists all event types to be watched. | +| [sources.k8s-create-events.botkube/kubernetes.config.resources](./values.yaml#L423) | list | See the `values.yaml` file for full object. | Describes the Kubernetes resources you want to watch. | +| [sources.prometheus.botkube/prometheus.enabled](./values.yaml#L440) | bool | `false` | If true, enables `prometheus` source. | +| [sources.prometheus.botkube/prometheus.config.url](./values.yaml#L443) | string | `"http://localhost:9090"` | Prometheus endpoint without api version and resource. | +| [sources.prometheus.botkube/prometheus.config.ignoreOldAlerts](./values.yaml#L445) | bool | `true` | If set as true, Prometheus source plugin will not send alerts that is created before plugin start time. | +| [sources.prometheus.botkube/prometheus.config.alertStates](./values.yaml#L447) | list | `["firing","pending","inactive"]` | 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 | +| [sources.prometheus.botkube/prometheus.config.log](./values.yaml#L449) | object | `{"level":"info"}` | Logging configuration | +| [sources.prometheus.botkube/prometheus.config.log.level](./values.yaml#L451) | string | `"info"` | Log level | +| [executors](./values.yaml#L459) | 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.botkube/helm.enabled](./values.yaml#L465) | bool | `false` | If true, enables `helm` commands execution. | +| [executors.k8s-default-tools.botkube/helm.config.helmDriver](./values.yaml#L470) | string | `"secret"` | Allowed values are configmap, secret, memory. | +| [executors.k8s-default-tools.botkube/helm.config.helmConfigDir](./values.yaml#L472) | string | `"/tmp/helm/"` | Location for storing Helm configuration. | +| [executors.k8s-default-tools.botkube/helm.config.helmCacheDir](./values.yaml#L474) | 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#L483) | object | See the `values.yaml` file for full object including optional properties related to interactive builder. | Custom kubectl configuration. | +| [aliases](./values.yaml#L508) | 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#L528) | 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#L535) | 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#L540) | bool | `false` | If true, enables Slack bot. | +| [communications.default-group.socketSlack.channels](./values.yaml#L544) | 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#L547) | 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#L550) | list | `["k8s-default-tools"]` | Executors configuration for a given channel. | +| [communications.default-group.socketSlack.channels.default.bindings.sources](./values.yaml#L553) | list | `["k8s-err-events","k8s-recommendation-events"]` | Notification sources configuration for a given channel. | +| [communications.default-group.socketSlack.botToken](./values.yaml#L558) | 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#L561) | 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#L565) | bool | `false` | If true, enables Mattermost bot. | +| [communications.default-group.mattermost.botName](./values.yaml#L567) | string | `"Botkube"` | User in Mattermost which belongs the specified Personal Access token. | +| [communications.default-group.mattermost.url](./values.yaml#L569) | 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#L571) | string | `"MATTERMOST_TOKEN"` | Personal Access token generated by Botkube user. | +| [communications.default-group.mattermost.team](./values.yaml#L573) | string | `"MATTERMOST_TEAM"` | The Mattermost Team name where Botkube is added. | +| [communications.default-group.mattermost.channels](./values.yaml#L577) | 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#L581) | 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#L584) | 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#L587) | list | `["k8s-default-tools"]` | Executors configuration for a given channel. | +| [communications.default-group.mattermost.channels.default.bindings.sources](./values.yaml#L590) | list | `["k8s-err-events","k8s-recommendation-events"]` | Notification sources configuration for a given channel. | +| [communications.default-group.teams.enabled](./values.yaml#L597) | bool | `false` | If true, enables MS Teams bot. | +| [communications.default-group.teams.botName](./values.yaml#L599) | string | `"Botkube"` | The Bot name set while registering Bot to MS Teams. | +| [communications.default-group.teams.appID](./values.yaml#L601) | string | `"APPLICATION_ID"` | The Botkube application ID generated while registering Bot to MS Teams. | +| [communications.default-group.teams.appPassword](./values.yaml#L603) | string | `"APPLICATION_PASSWORD"` | The Botkube application password generated while registering Bot to MS Teams. | +| [communications.default-group.teams.bindings.executors](./values.yaml#L606) | 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#L609) | 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#L613) | string | `"/bots/teams"` | The path in endpoint URL provided while registering Botkube to MS Teams. | +| [communications.default-group.teams.port](./values.yaml#L615) | int | `3978` | The Service port for bot endpoint on Botkube container. | +| [communications.default-group.discord.enabled](./values.yaml#L620) | bool | `false` | If true, enables Discord bot. | +| [communications.default-group.discord.token](./values.yaml#L622) | string | `"DISCORD_TOKEN"` | Botkube Bot Token. | +| [communications.default-group.discord.botID](./values.yaml#L624) | string | `"DISCORD_BOT_ID"` | Botkube Application Client ID. | +| [communications.default-group.discord.channels](./values.yaml#L628) | 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#L632) | 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#L635) | 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#L638) | list | `["k8s-default-tools"]` | Executors configuration for a given channel. | +| [communications.default-group.discord.channels.default.bindings.sources](./values.yaml#L641) | list | `["k8s-err-events","k8s-recommendation-events"]` | Notification sources configuration for a given channel. | +| [communications.default-group.elasticsearch.enabled](./values.yaml#L648) | bool | `false` | If true, enables Elasticsearch. | +| [communications.default-group.elasticsearch.awsSigning.enabled](./values.yaml#L652) | 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#L654) | string | `"us-east-1"` | AWS region where Elasticsearch is deployed. | +| [communications.default-group.elasticsearch.awsSigning.roleArn](./values.yaml#L656) | 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#L658) | string | `"ELASTICSEARCH_ADDRESS"` | The server URL, e.g https://example.com:9243 | +| [communications.default-group.elasticsearch.username](./values.yaml#L660) | string | `"ELASTICSEARCH_USERNAME"` | Basic Auth username. | +| [communications.default-group.elasticsearch.password](./values.yaml#L662) | string | `"ELASTICSEARCH_PASSWORD"` | Basic Auth password. | +| [communications.default-group.elasticsearch.skipTLSVerify](./values.yaml#L665) | 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#L669) | 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#L672) | string | `"botkube"` | Configures Elasticsearch index settings. | +| [communications.default-group.elasticsearch.indices.default.bindings.sources](./values.yaml#L678) | list | `["k8s-err-events","k8s-recommendation-events"]` | Notification sources configuration for a given index. | +| [communications.default-group.webhook.enabled](./values.yaml#L685) | bool | `false` | If true, enables Webhook. | +| [communications.default-group.webhook.url](./values.yaml#L687) | string | `"WEBHOOK_URL"` | The Webhook URL, e.g.: https://example.com:80 | +| [communications.default-group.webhook.bindings.sources](./values.yaml#L690) | list | `["k8s-err-events","k8s-recommendation-events"]` | Notification sources configuration for the webhook. | +| [communications.default-group.slack](./values.yaml#L700) | 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#L718) | string | `"not-configured"` | Cluster name to differentiate incoming messages. | +| [settings.lifecycleServer](./values.yaml#L721) | object | `{"enabled":true,"port":2113}` | Server configuration which exposes functionality related to the app lifecycle. | +| [settings.healthPort](./values.yaml#L724) | int | `2114` | | +| [settings.upgradeNotifier](./values.yaml#L726) | bool | `true` | If true, notifies about new Botkube releases. | +| [settings.log.level](./values.yaml#L730) | string | `"info"` | Sets one of the log levels. Allowed values: `info`, `warn`, `debug`, `error`, `fatal`, `panic`. | +| [settings.log.disableColors](./values.yaml#L732) | bool | `false` | If true, disable ANSI colors in logging. | +| [settings.systemConfigMap](./values.yaml#L735) | object | `{"name":"botkube-system"}` | Botkube's system ConfigMap where internal data is stored. | +| [settings.persistentConfig](./values.yaml#L740) | 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#L755) | bool | `false` | If true, specify cert path in `config.ssl.cert` property or K8s Secret in `config.ssl.existingSecretName`. | +| [ssl.existingSecretName](./values.yaml#L761) | string | `""` | Using existing SSL Secret. It MUST be in `botkube` Namespace. | +| [ssl.cert](./values.yaml#L764) | string | `""` | SSL Certificate file e.g certs/my-cert.crt. | +| [service](./values.yaml#L767) | object | `{"name":"metrics","port":2112,"targetPort":2112}` | Configures Service settings for ServiceMonitor CR. | +| [ingress](./values.yaml#L774) | 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#L785) | 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#L795) | object | `{}` | Extra annotations to pass to the Botkube Deployment. | +| [extraAnnotations](./values.yaml#L802) | object | `{}` | Extra annotations to pass to the Botkube Pod. | +| [extraLabels](./values.yaml#L804) | object | `{}` | Extra labels to pass to the Botkube Pod. | +| [priorityClassName](./values.yaml#L806) | string | `""` | Priority class name for the Botkube Pod. | +| [nameOverride](./values.yaml#L809) | string | `""` | Fully override "botkube.name" template. | +| [fullnameOverride](./values.yaml#L811) | string | `""` | Fully override "botkube.fullname" template. | +| [resources](./values.yaml#L817) | 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#L829) | 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#L843) | 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#L858) | 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#L876) | object | `{}` | Node labels for Botkube Pod assignment. [Ref doc](https://kubernetes.io/docs/user-guide/node-selection/). | +| [tolerations](./values.yaml#L880) | list | `[]` | Tolerations for Botkube Pod assignment. [Ref doc](https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/). | +| [affinity](./values.yaml#L884) | 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#L888) | bool | `true` | If true, a ServiceAccount is automatically created. | +| [serviceAccount.name](./values.yaml#L891) | string | `""` | The name of the service account to use. If not set, a name is generated using the fullname template. | +| [serviceAccount.annotations](./values.yaml#L893) | object | `{}` | Extra annotations for the ServiceAccount. | +| [extraObjects](./values.yaml#L896) | list | `[]` | Extra Kubernetes resources to create. Helm templating is allowed as it is evaluated before creating the resources. | +| [analytics.disable](./values.yaml#L924) | 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#L929) | bool | `true` | If true, restarts the Botkube Pod on config changes. | +| [configWatcher.tmpDir](./values.yaml#L931) | string | `"/tmp/watched-cfg/"` | Directory, where watched configuration resources are stored. | +| [configWatcher.initialSyncTimeout](./values.yaml#L934) | 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#L937) | string | `"ghcr.io"` | Config watcher image registry. | +| [configWatcher.image.repository](./values.yaml#L939) | string | `"kubeshop/k8s-sidecar"` | Config watcher image repository. | +| [configWatcher.image.tag](./values.yaml#L941) | string | `"ignore-initial-events"` | Config watcher image tag. | +| [configWatcher.image.pullPolicy](./values.yaml#L943) | string | `"IfNotPresent"` | Config watcher image pull policy. | +| [plugins](./values.yaml#L946) | 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#L948) | string | `"/tmp"` | Directory, where downloaded plugins are cached. | +| [plugins.repositories](./values.yaml#L950) | 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#L952) | 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#L956) | object | `{"provider":{"apiKey":"","endpoint":"https://api.botkube.io/graphql","identifier":""}}` | Configuration for synchronizing Botkube configuration. | +| [config.provider](./values.yaml#L958) | object | `{"apiKey":"","endpoint":"https://api.botkube.io/graphql","identifier":""}` | Base provider definition. | +| [config.provider.identifier](./values.yaml#L961) | string | `""` | Unique identifier for remote Botkube settings. If set to an empty string, Botkube won't fetch remote configuration. | +| [config.provider.endpoint](./values.yaml#L963) | string | `"https://api.botkube.io/graphql"` | Endpoint to fetch Botkube settings from. | +| [config.provider.apiKey](./values.yaml#L965) | 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 114760acea..e4d11dda1f 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,67 @@ 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" ] + user: #TODO: remove + type: Static + static: + value: "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" ] + user: #TODO: remove + type: Static + static: + value: "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" ] + user: #TODO: remove + type: Static + static: + value: "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" ] + user: #TODO: remove + type: Static + static: + value: "kc-label-svc-all" - 'plugin-based': + 'other-plugins': botkube/echo@v1.0.1-devel: enabled: true config: @@ -327,3 +318,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 ad10086fd9..5b28c69b00 100644 --- a/helm/botkube/values.yaml +++ b/helm/botkube/values.yaml @@ -117,7 +117,6 @@ sources: # @default -- See the `values.yaml` file for full object. botkube/kubernetes: context: &defaultPluginContext - defaultNamespace: "default" # -- RBAC configuration for this plugin. rbac: # -- Static impersonation for a given username and groups. @@ -459,32 +458,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 84ecc1f713..a7843cde60 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 36b3414f0c..7d2541435f 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 bd13ec8198..ccccf82051 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 { @@ -37,7 +38,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 a9f896ee16..688d7a44ec 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 4ba3a47bb5..f28dd420d3 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 ff7d89c5d4..0336cda9a8 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 82ef45caad..c7e439b008 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 6ccada5065..0d0b659a90 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 dbabd6e4f6..58a19ac151 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 48c0ac6f6a..534afb1172 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 87c4f804c1..dfd79ee687 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 cee84708cf..7aa2826257 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 1b6c7bc239..2d36a36206 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 18431ee4aa..9582f8b197 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 c2c7283c2c..728f3e6b6b 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 f9ee9a63d2..96f4a66bdf 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 1b9bf5d9db..3d40a8a8d5 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 4468498a49..32fc3eca64 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 dbc2076005..c2860bab17 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 a3c617e50e..52ff8c6aac 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 7415648071..0000000000 --- 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 fe903b4978..d886f76ab0 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 119eb347fe..0a34c21fcd 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 2a8812a2b6..0000000000 --- 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 7d5ffefbdb..c89cdd8ec7 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 b45dca9a9d..df03399314 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 da628c67fe..5464e9ce31 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 e291f80eed..daa1507405 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 896d402b53..0000000000 --- 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 5c7a66f6f2..0000000000 --- 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 224c86e00d..0000000000 --- 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 b68f085cf3..0000000000 --- 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 a41c786c83..0000000000 --- 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 db7bfebe9a..0000000000 --- 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 846347f151..0000000000 --- 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 ad62733188..0000000000 --- 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 5d1bd380b1..0000000000 --- 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 8882214222..0000000000 --- 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 24f1dffe0b..0000000000 --- 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 74cd1ff3cc..0000000000 --- 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 405921a05b..0000000000 --- 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 a28bb205d1..a5c2165b81 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 0000000000..8f52163c38 --- /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 52976695de..5506c7a8a6 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 c568cf960f..8c71425a50 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 feb97c2016..5f54d81ad5 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 2917cd119f..7911c781dd 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")