├── Dockerfile ├── README.md ├── img └── slack.png ├── kubectl-deployment.yaml └── main.go /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.6.2-onbuild 2 | 3 | ENV KUBECTL_VERSION v1.2.1 4 | ENV KUBECTL_SHA256 a41b9543ddef1f64078716075311c44c6e1d02c67301c0937a658cef37923bbb 5 | 6 | ADD https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl /bin/kubectl 7 | 8 | RUN echo "${KUBECTL_SHA256} */bin/kubectl" | sha256sum -c - \ 9 | && chmod ugo+x /bin/kubectl 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated 2 | 3 | This was just a fun side project I did a couple years back during my first couple weeks of learning go. Don't use this. Seriously. 4 | 5 | # Kubectl Slackbot 6 | 7 | ![kubectl slackbot being used in the slack client](img/slack.png) 8 | 9 | Kubectl Slackbot is a bot based on [nlopes slack api](github.com/nlopes/slack). Kubectl Slackbot allows you to run kubectl commands in slack. 10 | 11 | ## Install 12 | 13 | To install on a running kubernetes cluster just create a slack API token and update the "SLACK_API_TOKEN" environment var in the kubectl-deployment.yaml file. Once completed, just run the following: 14 | 15 | ``` 16 | kubectl create -f kubectl-deployment.yaml 17 | ``` 18 | 19 | Once running, invite the bot into a slack room and run commands as you normally would via kubectl. 20 | 21 | ## Coming soon 22 | 23 | Currently this is alpha quality code which probably shouldn't be used for anything too serious. Future plans include the following 24 | 25 | * Moving the SLACK_API_TOKEN to a proper kubernetes secret in the example manifest 26 | * Only allow whitelisted users to run kubectl commands 27 | * More sane error handling (i.e. don't allow users to use -f flag for logs, and other stuff) 28 | * Better handling of large outputs. I.e. only print the last N lines, or post it as a snippet. 29 | 30 | -------------------------------------------------------------------------------- /img/slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beeradb/kubectl-slackbot/8ea05d940c8e7ec23f983f9d1e67682315a3ab0e/img/slack.png -------------------------------------------------------------------------------- /kubectl-deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: kubectl 6 | --- 7 | apiVersion: v1 8 | kind: ServiceAccount 9 | metadata: 10 | name: kubectl 11 | namespace: kubectl 12 | --- 13 | apiVersion: extensions/v1beta1 14 | kind: Deployment 15 | metadata: 16 | name: kubectl 17 | namespace: kubectl 18 | spec: 19 | strategy: 20 | type: Recreate 21 | template: 22 | metadata: 23 | namespace: kubectl 24 | labels: 25 | app: kubectl 26 | spec: 27 | serviceAccount: kubectl 28 | containers: 29 | - image: quay.io/beeradb/kubectl-slackbot:latest 30 | name: kubectl-slackbot 31 | env: 32 | - name: SLACK_API_TOKEN 33 | value: TOKEN HERE 34 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "os/exec" 8 | "reflect" 9 | "strings" 10 | 11 | "github.com/mattn/go-shellwords" 12 | "github.com/nlopes/slack" 13 | "gopkg.in/yaml.v2" 14 | ) 15 | 16 | type WhiteList struct { 17 | Users []string 18 | } 19 | 20 | func main() { 21 | api := slack.New(os.Getenv("SLACK_API_TOKEN")) 22 | rtm := api.NewRTM() 23 | 24 | whiteList := GetWhiteList() 25 | go rtm.ManageConnection() 26 | 27 | fmt.Println(reflect.TypeOf(api)) 28 | var UserID string 29 | Loop: 30 | for { 31 | select { 32 | case msg := <-rtm.IncomingEvents: 33 | switch ev := msg.Data.(type) { 34 | case *slack.HelloEvent: 35 | // Ignore hello 36 | 37 | case *slack.ConnectedEvent: 38 | UserID = ev.Info.User.ID 39 | 40 | case *slack.MessageEvent: 41 | username := fmt.Sprintf("<@%s>", UserID) 42 | fmt.Println(ev.User) 43 | if strings.Contains(ev.Text, username) { 44 | if CheckWhitelist(ev.User, whiteList, api) { 45 | ProcessMessage(ev, username, rtm, api) 46 | } 47 | } 48 | case *slack.InvalidAuthEvent: 49 | fmt.Printf("Invalid credentials") 50 | break Loop 51 | 52 | default: 53 | 54 | } 55 | } 56 | } 57 | } 58 | 59 | func ProcessMessage(ev *slack.MessageEvent, username string, rtm *slack.RTM, api *slack.Client) { 60 | command := strings.Trim(strings.TrimPrefix(ev.Text, username), " ") 61 | command = strings.Replace(command, "—", "--", -1) 62 | 63 | if command[0:1] == ":" { 64 | command = command[1:] 65 | } 66 | 67 | fmt.Println(command) 68 | result := kubectl(command) 69 | fmt.Println("len is ", strings.Count(result, "\n")) 70 | 71 | if strings.Count(result, "\n") > 80 { 72 | fmt.Println("Sending file") 73 | File(result, ev.Channel, api) 74 | } else { 75 | fmt.Println("Sending message") 76 | Message(result, ev.Channel, rtm) 77 | } 78 | } 79 | 80 | func GetWhiteList() WhiteList { 81 | var whiteList WhiteList 82 | buf, err := ioutil.ReadFile(os.Getenv("SLACK_WHITELIST_CONFIG")) 83 | 84 | if err != nil { 85 | return whiteList 86 | } 87 | 88 | yaml.Unmarshal(buf, &whiteList) 89 | fmt.Println(whiteList) 90 | return whiteList 91 | } 92 | 93 | func CheckWhitelist(userId string, whiteList WhiteList, api *slack.Client) bool { 94 | 95 | user, err := api.GetUserInfo(userId) 96 | if err != nil { 97 | fmt.Println("User not found.") 98 | // If there is no user then it certainly isn't whitelisted. 99 | return false 100 | } 101 | 102 | if len(whiteList.Users) == 0 { 103 | fmt.Println("No whitelist available.") 104 | // If no whitelist exists, then allow everything. 105 | return true 106 | } 107 | 108 | for _, name := range whiteList.Users { 109 | if name == user.Name { 110 | fmt.Println("found match") 111 | return true 112 | } 113 | } 114 | 115 | fmt.Println("Whitelist could not be matched for user: ", user.Name) 116 | return false 117 | } 118 | 119 | // Send a message to a slack channel using the real time messaging api. 120 | func Message(result string, channel string, rtm *slack.RTM) { 121 | rtm.SendMessage(rtm.NewOutgoingMessage(fmt.Sprintf("```%s```", result), channel)) 122 | } 123 | 124 | // Attach a result string as a file to a slack channel. 125 | func File(result string, channel string, api *slack.Client) { 126 | params := slack.FileUploadParameters{ 127 | Title: "Kubectl result", 128 | Filetype: "shell", 129 | File: "sh", 130 | Channels: []string{channel}, 131 | Content: result, 132 | } 133 | 134 | file, err := api.UploadFile(params) 135 | if err != nil { 136 | fmt.Printf("%s\n", err) 137 | return 138 | } 139 | fmt.Printf("Name: %s, URL: %s\n", file.Name, file.URL) 140 | } 141 | 142 | // Run an arbitrary string as a kubectl command. 143 | func kubectl(command string) string { 144 | buf, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token") 145 | if err != nil { 146 | panic(err) 147 | } 148 | token := string(buf) 149 | 150 | caPath := "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" 151 | server := kube_api_url() 152 | shellCommand := fmt.Sprintf("%s --token=%s --server=%s --certificate-authority=%s", command, token, server, caPath) 153 | p := shellwords.NewParser() 154 | p.ParseBacktick = true 155 | args, err := p.Parse(shellCommand) 156 | 157 | if err != nil { 158 | return fmt.Sprintf("There was an error parsing your command") 159 | } 160 | 161 | cmdOut, err := exec.Command("/bin/kubectl", args...).Output() 162 | if err != nil { 163 | return fmt.Sprintf("There was an error running the kubectl command: ", err) 164 | } 165 | 166 | return string(cmdOut) 167 | } 168 | 169 | // Format the kubernetes API URL. 170 | func kube_api_url() string { 171 | return fmt.Sprintf( 172 | "https://%s:%s", 173 | os.Getenv("KUBERNETES_SERVICE_HOST"), 174 | os.Getenv("KUBERNETES_SERVICE_PORT"), 175 | ) 176 | } 177 | --------------------------------------------------------------------------------