Skip to content

Commit

Permalink
Command works with singular and short noun
Browse files Browse the repository at this point in the history
Signed-off-by: Prasad Ghangal <prasad.ghangal@gmail.com>
  • Loading branch information
PrasadG193 committed Jun 21, 2020
1 parent 1a1f07e commit f9ac20c
Show file tree
Hide file tree
Showing 10 changed files with 111 additions and 100 deletions.
3 changes: 2 additions & 1 deletion cmd/botkube/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ func main() {

// Init KubeClient, InformerMap and start controller
utils.InitKubeClient()
utils.InitInformerMap()
utils.InitInformerMap(conf)
utils.InitResourceMap(conf)
controller.RegisterInformers(conf, notifiers)
}

Expand Down
5 changes: 5 additions & 0 deletions deploy-all-in-one-tls.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,11 @@ data:
kubectl:
# Set true to enable kubectl commands execution
enabled: false
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
Expand Down
5 changes: 5 additions & 0 deletions deploy-all-in-one.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,11 @@ data:
kubectl:
# Set true to enable kubectl commands execution
enabled: false
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
Expand Down
8 changes: 7 additions & 1 deletion helm/botkube/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ image:
pullPolicy: IfNotPresent
## default tag is appVersion from Chart.yaml. If you want to use
## some other tag then it can be specified here
tag: v0.10.0
tag: latest

nameOverride: ""
fullnameOverride: ""
Expand Down Expand Up @@ -231,6 +231,12 @@ config:
kubectl:
# 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
Expand Down
6 changes: 1 addition & 5 deletions pkg/bot/mattermost.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,6 @@ type MMBot struct {
WSClient *model.WebSocketClient
APIClient *model.Client4
DefaultNamespace string
Verbs []string
Resources []string
}

// mattermostMessage contains message details to execute command and send back the result
Expand All @@ -84,8 +82,6 @@ func NewMattermostBot(c *config.Config) Bot {
AllowKubectl: c.Settings.Kubectl.Enabled,
RestrictAccess: c.Settings.Kubectl.RestrictAccess,
DefaultNamespace: c.Settings.Kubectl.DefaultNamespace,
Verbs: c.Settings.Kubectl.Commands.Verbs,
Resources: c.Settings.Kubectl.Commands.Resources,
}
}

Expand Down Expand Up @@ -154,7 +150,7 @@ func (mm *mattermostMessage) handleMessage(b MMBot) {
mm.Request = strings.TrimPrefix(post.Message, "@"+BotName+" ")

e := execute.NewDefaultExecutor(mm.Request, b.AllowKubectl, b.RestrictAccess, b.DefaultNamespace,
b.ClusterName, b.ChannelName, mm.IsAuthChannel, b.Verbs, b.Resources)
b.ClusterName, b.ChannelName, mm.IsAuthChannel)
mm.Response = e.Execute()
mm.sendMessage()
}
Expand Down
6 changes: 1 addition & 5 deletions pkg/bot/slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ type SlackBot struct {
SlackURL string
BotID string
DefaultNamespace string
Verbs []string
Resources []string
}

// slackMessage contains message details to execute command and send back the result
Expand All @@ -62,8 +60,6 @@ func NewSlackBot(c *config.Config) Bot {
ClusterName: c.Settings.ClusterName,
ChannelName: c.Communications.Slack.Channel,
DefaultNamespace: c.Settings.Kubectl.DefaultNamespace,
Verbs: c.Settings.Kubectl.Commands.Verbs,
Resources: c.Settings.Kubectl.Commands.Resources,
}
}

Expand Down Expand Up @@ -160,7 +156,7 @@ func (sm *slackMessage) HandleMessage(b *SlackBot) {
}

e := execute.NewDefaultExecutor(sm.Request, b.AllowKubectl, b.RestrictAccess, b.DefaultNamespace,
b.ClusterName, b.ChannelName, sm.IsAuthChannel, b.Verbs, b.Resources)
b.ClusterName, b.ChannelName, sm.IsAuthChannel)
sm.Response = e.Execute()
sm.Send()
}
Expand Down
73 changes: 42 additions & 31 deletions pkg/execute/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,23 +36,33 @@ import (
"github.com/infracloudio/botkube/pkg/utils"
)

var validNotifierCommand = map[string]bool{
"notifier": true,
}

var validPingCommand = map[string]bool{
"ping": true,
}

var validVersionCommand = map[string]bool{
"version": true,
}

var validFilterCommand = map[string]bool{
"filters": true,
}
var (
validNotifierCommand = map[string]bool{
"notifier": true,
}
validPingCommand = map[string]bool{
"ping": true,
}
validVersionCommand = map[string]bool{
"version": true,
}
validFilterCommand = map[string]bool{
"filters": true,
}
validDebugCommands = map[string]bool{
"exec": true,
"logs": true,
"attach": true,
"auth": true,
"api-versions": true,
"cluster-info": true,
"cordon": true,
"drain": true,
"uncordon": true,
}

var kubectlBinary = "/usr/local/bin/kubectl"
kubectlBinary = "/usr/local/bin/kubectl"
)

const (
notifierStartMsg = "Brace yourselves, notifications are coming from cluster '%s'."
Expand All @@ -79,8 +89,6 @@ type DefaultExecutor struct {
ChannelName string
IsAuthChannel bool
DefaultNamespace string
Verbs []string
Resources []string
}

// CommandRunner is an interface to run bash commands
Expand Down Expand Up @@ -135,7 +143,7 @@ func (action FiltersAction) String() string {

// NewDefaultExecutor returns new Executor object
func NewDefaultExecutor(msg string, allowkubectl, restrictAccess bool, defaultNamespace,
clusterName, channelName string, isAuthChannel bool, verbs []string, resources []string) Executor {
clusterName, channelName string, isAuthChannel bool) Executor {
return &DefaultExecutor{
Message: msg,
AllowKubectl: allowkubectl,
Expand All @@ -144,28 +152,31 @@ func NewDefaultExecutor(msg string, allowkubectl, restrictAccess bool, defaultNa
ChannelName: channelName,
IsAuthChannel: isAuthChannel,
DefaultNamespace: defaultNamespace,
Verbs: verbs,
Resources: resources,
}
}

// Execute executes commands and returns output
func (e *DefaultExecutor) Execute() string {
args := strings.Fields(e.Message)

if utils.Contains(e.Verbs, args[0]) && utils.Contains(e.Resources, args[1]) {
isClusterNamePresent := strings.Contains(e.Message, "--cluster-name")
if !e.AllowKubectl {
if isClusterNamePresent && e.ClusterName == utils.GetClusterNameFromKubectlCmd(e.Message) {
return fmt.Sprintf(kubectlDisabledMsg, e.ClusterName)
if len(args) >= 2 && utils.AllowedKubectlVerbMap[args[0]] {
if validDebugCommands[args[0]] || // Don't check for resource if is a valid debug command
utils.AllowedKubectlResourceMap[args[1]] || // Check if allowed resource
utils.AllowedKubectlResourceMap[utils.KindResourceMap[strings.ToLower(args[1])]] || // Check if matches with kind name
utils.AllowedKubectlResourceMap[utils.ShortnameResourceMap[strings.ToLower(args[1])]] { // Check if matches with short name
isClusterNamePresent := strings.Contains(e.Message, "--cluster-name")
if !e.AllowKubectl {
if isClusterNamePresent && e.ClusterName == utils.GetClusterNameFromKubectlCmd(e.Message) {
return fmt.Sprintf(kubectlDisabledMsg, e.ClusterName)
}
return ""
}
return ""
}

if e.RestrictAccess && !e.IsAuthChannel && isClusterNamePresent {
return ""
if e.RestrictAccess && !e.IsAuthChannel && isClusterNamePresent {
return ""
}
return runKubectlCommand(args, e.ClusterName, e.DefaultNamespace, e.IsAuthChannel)
}
return runKubectlCommand(args, e.ClusterName, e.DefaultNamespace, e.IsAuthChannel)
}
if validNotifierCommand[args[0]] {
return runNotifierCommand(args, e.ClusterName, e.IsAuthChannel)
Expand Down
64 changes: 46 additions & 18 deletions pkg/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@
package utils

import (
"fmt"
//"fmt"
"os"
"regexp"
"strconv"
"strings"
"time"

"github.com/infracloudio/botkube/pkg/config"
Expand All @@ -46,8 +47,16 @@ var (
ResourceInformerMap map[string]cache.SharedIndexInformer
// AllowedEventKindsMap is a map to filter valid event kinds
AllowedEventKindsMap map[EventKind]bool
// AllowedUpdateEventsMap is a map of resourceand namespace to updateconfig
// AllowedUpdateEventsMap is a map of resource and namespace to updateconfig
AllowedUpdateEventsMap map[KindNS]config.UpdateSetting
// AllowedKubectlResourceMap is map of allowed resources with kubectl command
AllowedKubectlResourceMap map[string]bool
// AllowedKubectlVerbMap is map of allowed verb with kubectl command
AllowedKubectlVerbMap map[string]bool
// KindResourceMap contains resource name to kind mapping
KindResourceMap map[string]string
// ShortnameResourceMap contains resource name to short name mapping
ShortnameResourceMap map[string]string
// KubeClient is a global kubernetes client to communicate to apiserver
KubeClient kubernetes.Interface
// KubeInformerFactory is a global SharedInformerFactory object to watch resources
Expand Down Expand Up @@ -92,12 +101,7 @@ type KindNS struct {
}

// InitInformerMap initializes helper maps to filter events
func InitInformerMap() {
conf, err := config.New()
if err != nil {
log.Fatal(fmt.Sprintf("Error in loading configuration. Error:%s", err.Error()))
}

func InitInformerMap(conf *config.Config) {
// Get resync period
rsyncTimeStr, ok := os.LookupEnv("INFORMERS_RESYNC_PERIOD")
if !ok {
Expand Down Expand Up @@ -423,6 +427,40 @@ func ExtractAnnotaions(obj *coreV1.Event) map[string]string {
return map[string]string{}
}

func InitResourceMap(conf *config.Config) {
KindResourceMap = make(map[string]string)
ShortnameResourceMap = make(map[string]string)
AllowedKubectlResourceMap = make(map[string]bool)
AllowedKubectlVerbMap = make(map[string]bool)

for _, r := range conf.Settings.Kubectl.Commands.Resources {
AllowedKubectlResourceMap[r] = true
}
for _, r := range conf.Settings.Kubectl.Commands.Verbs {
AllowedKubectlVerbMap[r] = true
}

resourceList, err := KubeClient.Discovery().ServerResources()
if err != nil {
log.Errorf("Failed to get resource list in k8s cluster. %v", err)
return
}
for _, resource := range resourceList {
for _, r := range resource.APIResources {
// Ignore subresources
if strings.Contains(r.Name, "/") {
continue
}
KindResourceMap[strings.ToLower(r.Kind)] = r.Name
for _, sn := range r.ShortNames {
ShortnameResourceMap[sn] = r.Name
}
}
}
log.Infof("KindResourceMap - %+v", KindResourceMap)
log.Infof("ShortnameResourceMap - %+v", ShortnameResourceMap)
}

//GetClusterNameFromKubectlCmd this will return cluster name from kubectl command
func GetClusterNameFromKubectlCmd(cmd string) string {
r, _ := regexp.Compile(`--cluster-name[=|' ']([^\s]*)`)
Expand All @@ -434,13 +472,3 @@ func GetClusterNameFromKubectlCmd(cmd string) string {
}
return s
}

// Contains tells whether a contains x.
func Contains(a []string, x string) bool {
for _, n := range a {
if x == n {
return true
}
}
return false
}
37 changes: 0 additions & 37 deletions pkg/utils/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,40 +49,3 @@ func TestGetClusterNameFromKubectlCmd(t *testing.T) {
}
}
}

func TestContainsMethod(t *testing.T) {
type input struct {
array []string
value string
}
type test struct {
input input
expected bool
}

input1 := input{
array: []string{"get", "logs"},
value: "logs",
}
input2 := input{
array: []string{"get", "logs"},
value: "describe",
}
tests := []test{
{
input: input1,
expected: true,
},
{
input: input2,
expected: false,
},
}

for _, ts := range tests {
got := Contains(ts.input.array, ts.input.value)
if got != ts.expected {
t.Errorf("expected: %v, got: %v", ts.expected, got)
}
}
}
4 changes: 2 additions & 2 deletions resource_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,9 @@ settings:
# List of allowed commands
commands:
# method which are allowed
verbs: ["api-resources","api-versions","cluster-info","describe","diff","explain","get","logs","top","auth"]
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", "serviceaccounts", "deployment", "pod" , "namespace", "daemonset", "statefulset", "storageclasse", "serviceaccount"]
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
Expand Down

0 comments on commit f9ac20c

Please sign in to comment.