Статьи

Создание прокси Slack Docker в Go — часть 1

В прошлом году у меня была возможность работать с множеством крутых инструментов и технологий. Пару из них были иди , вялый и докер . Мы в значительной степени используем slack для всех наших коммуникаций и постепенно добавляем все больше и больше интеграций в slack для получения информации от различных инструментов сборки и выполнения. В нашей среде у нас есть несколько различных сред докеров, и в настоящее время либо войдите на хост-компьютер, чтобы проверить состояние докера, либо используйте docker-u i (который мы взломали для работы с несколькими средами докеров). Несмотря на то, что docker-ui отлично работает и предоставляет отличный способ получить представление о том, что происходит в конкретном демоне docker, он не очень легкий и в некоторых сценариях требует много кликов, чтобы получить информацию, которую разработчик находясь в поиске.

В качестве альтернативы разработчики могут просто использовать интерфейс Docker API, но для этого потребуется открыть много портов брандмауэра. Поэтому мы начали изучать использование (или создание) интеграции Slack, которую мы могли бы использовать для запроса наших сред докера непосредственно из Slack. Таким образом, разработчикам не нужно будет использовать новый инструмент, они уже почти все время открыты и могут быстро получить статус определенного контейнера докера (и, если позволит время, мы также добавим интеграцию Jenkins для развертываний в различные среды разработки от слабины).

Если ваша команда еще не использует Slack, вы можете начать бесплатно и использовать все интересные возможности Slack. Я работал в командах из 30+ человек, которые использовали одну и ту же бесплатную версию Slack, так что нет смысла не использовать Slack. Так что достаточно слабого продвижения, давайте начнем.

Отправка команд из слабины

Slack предоставляет несколько различных способов интеграции с внешними системами. В этом сценарии мы будем использовать « Командную черту ». С помощью команд косой черты мы можем просто ввести нашу команду в Slack, и Slack отправит сообщение POST с приложением типа контента / x-www-form-urlencoded в сконфигурированную конечную точку, которая содержит такие данные:

01
02
03
04
05
06
07
08
09
10
token=gIkuvaNzQIHg97ATvDxqgjtO
team_id=T0001
team_domain=example
channel_id=C2147483705
channel_name=test
user_id=U2147483697
user_name=Steve
command=/weather
text=94070
response_url=https://hooks.slack.com/commands/1234/5678

Теперь мы можем либо ответить напрямую, либо отправить ответ на предоставленный response_url. В этой статье мы просто ответим напрямую (что необходимо сделать в течение 3000 мс). Итак, во-первых, давайте настроим slack для отправки определенной команды:

Докер-слабины-1

В этом случае всякий раз, когда мы набираем / docker, команда отправляется на нашу настроенную конечную точку. Например, если мы введем «/ docker CI ps», мы получим обзор всех изображений, работающих в среде CI:

Докер-слабины-2

В следующих двух разделах мы покажем вам, как реализовать такой прокси с помощью go-lang.

Создание прокси-сервера

Весь код отсюда можно найти на моем github ( https://github.com/josdirksen/slack-proxy ), и если вы просто хотите запустить его, просто выполните следующие простые шаги:

1
2
3
4
5
6
7
8
9
$ git clone https://github.com/josdirksen/slack-proxy
$ cd slack-proxy
$ export GOPATH=`pwd`
$ go get github.com/fsouza/go-dockerclient
$ go build src/github.com/
$ go build src/github.com/josdirksen/slackproxy/slack-proxy.go
# Make sure to edit the config.json to point to the correct docker location and keys/certificates
$ ./slack-proxy -config resources/config.json
{5cLHiZjpWaRDb0fP6ka02XCR {[{local tcp://192.168.99.100:2376 true /Users/jos/.docker/machine/machines/eris ca.pem cert.pem key.pem}]}}

Давайте посмотрим на код. Первый файл, который мы рассмотрим, это основной файл slack-proxy.go:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main
 
import (
    "flag"
    "github.com/josdirksen/slackproxy/config"
    "github.com/josdirksen/slackproxy/handlers"
    "log"
    "net/http"
)
 
// setup the listener, with a config passed in.
func GetConfigListener(config *config.Configuration) func(http.ResponseWriter, *http.Request) {
    return func(w http.ResponseWriter, req *http.Request) {
        // get the contents from the request body, convert it to a command
        req.ParseForm()
        cmdToExecute := handlers.ParseInput(req.Form)
 
        // now check whether the token is valid
        if config.Token == cmdToExecute.Token {
            // execute the command
            handler := handlers.GetHandler("docker", config)
            handler.HandleCommand(cmdToExecute, w)
        } else {
            w.WriteHeader(406)
        }
 
    }
}
 
func StartListening(config *config.Configuration) {
    http.HandleFunc("/handleSlackCommand", GetConfigListener(config))
    err := http.ListenAndServe(":9000", nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}
 
func main() {
    var configLocation = flag.String("config", "config.json", "specify the config file")
    flag.Parse()
    // first parse the config
    config.ParseConfig(*configLocation)
    // setup the handler that listens to 9000
    StartListening(config.GetConfig())
}

Как вы можете видеть здесь, мы не так много делаем. Главное, что мы делаем, это анализируем конфигурацию и настраиваем прослушиватель HTTP на порт 9000. Поэтому всякий раз, когда мы получаем запрос на порт 9000, указанный обработчик, возвращаемый GetConfigListener, вызывается:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
// setup the listener, with a config passed in.
func GetConfigListener(config *config.Configuration) func(http.ResponseWriter, *http.Request) {
    return func(w http.ResponseWriter, req *http.Request) {
        // get the contents from the request body, convert it to a command
        req.ParseForm()
        cmdToExecute := handlers.ParseInput(req.Form)
 
        // now check whether the token is valid
        if config.Token == cmdToExecute.Token {
            // execute the command
            handler := handlers.GetHandler("docker", config)
            handler.HandleCommand(cmdToExecute, w)
        } else {
            w.WriteHeader(406)
        }
 
    }
}

Сам обработчик тоже не так уж и много делает. Он просто передает команду, которую необходимо выполнить, обработчику, указанному «docker». Обратите внимание, что на этом этапе мы поддерживаем только команды «docker». Но мы могли бы легко использовать эту настройку для обработки других команд. Подробнее об этом в следующей статье.

Обрабатывать команды

Итак, давайте посмотрим поближе на функцию handler.HandleCommand (которую вы можете найти в файле dockerCommandHandlers.go:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
package handlers
 
import (
    "io"
    "net/http"
    "fmt"
    "github.com/fsouza/go-dockerclient"
    "bytes"
    "time"
    "github.com/josdirksen/slackproxy/config"
    "log"
    "os"
)
 
type DockerHandler struct {
    config  *config.Configuration
}
 
func NewDockerHandler(configuration *config.Configuration) *DockerHandler {
    p := new(DockerHandler)
    p.config = configuration
    return p
}
 
func (dh DockerHandler) HandleCommand(cmdToExecute *Command, w http.ResponseWriter) {
    client := setupDockerClient(cmdToExecute.Environment)
 
    switch cmdToExecute.SlackCommand {
    case "ps" : handlePsCommand(client, w)
    case "imgs" : handleListImagesCommand(client, w)
    }
}
 
func handleListImagesCommand(client *docker.Client, w http.ResponseWriter) {
    images, _ := client.ListImages(docker.ListImagesOptions{All: false})
    for _, img := range images {
        fmt.Println("ID: ", img.ID)
        fmt.Println("RepoTags: ", img.RepoTags)
        fmt.Println("Created: ", img.Created)
        fmt.Println("Size: ", img.Size)
        fmt.Println("VirtualSize: ", img.VirtualSize)
        fmt.Println("ParentId: ", img.ParentID)
    }
}
 
func handlePsCommand(client *docker.Client, w http.ResponseWriter) {
    containers, _ := client.ListContainers(docker.ListContainersOptions{All: false})
    var buffer bytes.Buffer
    for _, container := range containers {
        buffer.WriteString(fmt.Sprintf("ID: %s\n", container.ID))
        buffer.WriteString(fmt.Sprintf("Command: %s\n", container.Command))
        buffer.WriteString(fmt.Sprintf("Created: %s\n", time.Unix(container.Created, 0)))
        buffer.WriteString(fmt.Sprintf("Image: %s\n", container.Image))
        buffer.WriteString(fmt.Sprintf("Status: %s\n", container.Status))
        buffer.WriteString(fmt.Sprintf("Names: %s\n", container.Names))
        if (len(container.Ports) > 0) {
            buffer.WriteString("Ports: \n")
            for _, port := range container.Ports {
 
                buffer.WriteString(fmt.Sprintf("\t type: %s IP: %s private: %d public: %d\n", port.Type, port.IP, port.PrivatePort, port.PublicPort))
            }
        }
        buffer.WriteString(fmt.Sprint("\n"))
    }
 
    io.WriteString(w, buffer.String())
}
 
func setupDockerClient(env string) *docker.Client {
 
    // first get the environment from the config
    cfg, err := config.GetDockerEnvironmentConfig(env)
 
    if (err != nil) {
        println(err)
        log.Fatal("Can't parse config, exiting")
        os.Exit(1)
    }
 
    if (cfg.Tls) {
        endpoint := cfg.Host
        path := cfg.Path
        ca := fmt.Sprintf("%s/%s", path, cfg.Ca)
        cert := fmt.Sprintf("%s/%s", path, cfg.Cert)
        key := fmt.Sprintf("%s/%s", path, cfg.Key)
 
        client,_ := docker.NewTLSClient(endpoint, cert, key, ca)
        return client
    } else {
        client,_ := docker.NewClient(cfg.Host)
        return client
    }
 
}

Много кода, но главное на что стоит обратить внимание — это функция HandleCommand. Здесь мы просто создаем новый клиент Docker (см. Код выше) и используем простой переключатель, чтобы определить, какую функцию вызывать.

1
2
3
4
5
6
7
8
func (dh DockerHandler) HandleCommand(cmdToExecute *Command, w http.ResponseWriter) {
    client := setupDockerClient(cmdToExecute.Environment)
 
    switch cmdToExecute.SlackCommand {
    case "ps" : handlePsCommand(client, w)
    case "imgs" : handleListImagesCommand(client, w)
    }
}

Например, если вы посмотрите немного ближе к handlePsCommand, вы увидите, что мы просто конвертируем ответ из функции client.ListContainers и записываем его в http.ResponseWriter.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
func handlePsCommand(client *docker.Client, w http.ResponseWriter) {
    containers, _ := client.ListContainers(docker.ListContainersOptions{All: false})
    var buffer bytes.Buffer
    for _, container := range containers {
        buffer.WriteString(fmt.Sprintf("ID: %s\n", container.ID))
        buffer.WriteString(fmt.Sprintf("Command: %s\n", container.Command))
        buffer.WriteString(fmt.Sprintf("Created: %s\n", time.Unix(container.Created, 0)))
        buffer.WriteString(fmt.Sprintf("Image: %s\n", container.Image))
        buffer.WriteString(fmt.Sprintf("Status: %s\n", container.Status))
        buffer.WriteString(fmt.Sprintf("Names: %s\n", container.Names))
        if (len(container.Ports) > 0) {
            buffer.WriteString("Ports: \n")
            for _, port := range container.Ports {
 
                buffer.WriteString(fmt.Sprintf("\t type: %s IP: %s private: %d public: %d\n", port.Type, port.IP, port.PrivatePort, port.PublicPort))
            }
        }
        buffer.WriteString(fmt.Sprint("\n"))
    }
 
    io.WriteString(w, buffer.String())
}

# Запуск и тестирование кода

Теперь мы можем построить и запустить код:

1
2
3
4
5
6
7
8
9
$ git clone https://github.com/josdirksen/slack-proxy
$ cd slack-proxy
$ export GOPATH=`pwd`
$ go get github.com/fsouza/go-dockerclient
$ go build src/github.com/
$ go build src/github.com/josdirksen/slackproxy/slack-proxy.go
# Make sure to edit the config.json to point to the correct docker location and keys/certificates
$ ./slack-proxy -config resources/config.json
{5cLHiZjpWaRDb0fP6ka02XCR {[{local tcp://192.168.99.100:2376 true /Users/jos/.docker/machine/machines/eris ca.pem cert.pem key.pem}]}}

И чтобы проверить это, мы используем curl для отправки запроса. Запрос, который мы тестируем, выглядит так:

1
$ curl 'http://localhost:9000/handleSlackCommand' -H 'Content-Type: application/x-www-form-urlencoded' --data 'token=5cLHiZjpWaRDb0fP6ka02XCR&team_id=T0001&team_domain=example&channel_id=C2147483705&channel_name=test&user_id=U2147483697&user_name=Steve,nd=/docker&text=local+ps'

Который возвращает информацию, как это:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
ID: 669265a1343601468e71fbe65dec0251595f6c043ceeb5748f6736b28fd5da39
Command: /bin/start -server -advertise 192.168.99.100 -bootstrap-expect 1
Created: 2016-01-06 08:05:53 +0100 CET
Image: docker-register.equeris.com/consul:eris-local
Status: Up 3 minutes
Names: [/consul]
Ports:
         type: tcp IP: 192.168.99.100 private: 8400 public: 8400
         type: tcp IP: 192.168.99.100 private: 8302 public: 8302
         type: tcp IP: 192.168.99.100 private: 8301 public: 8301
         type: udp IP: 192.168.99.100 private: 53 public: 53
         type: tcp IP:  private: 53 public: 0
         type: tcp IP: 192.168.99.100 private: 8500 public: 8500
         type: udp IP: 192.168.99.100 private: 8301 public: 8301
         type: tcp IP: 192.168.99.100 private: 8300 public: 8300
         type: udp IP: 192.168.99.100 private: 8302 public: 8302
  
ID: 682f47f97fced8725d228a68a4bdf5486bf0d3e247f6864331e4f6b413b92f65
Command: /find-ip-add-to-consul.sh -Dcassandra.load_ring_state=false
Created: 2015-11-24 13:29:38 +0100 CET
Image: docker-register.equeris.com/cassandra:eris
Status: Up 3 minutes
Names: [/cassandra-eris]
Ports:
         type: tcp IP: 192.168.99.100 private: 9160 public: 9160
         type: tcp IP: 192.168.99.100 private: 9042 public: 9042
         type: tcp IP:  private: 7199 public: 0
         type: tcp IP:  private: 7001 public: 0
         type: tcp IP: 192.168.99.100 private: 7000 public: 7000

Вот именно для этой статьи. Обратите внимание, что мы упустили много вещей, мы только реализовали команду ps, не проводя много проверки или обработки ошибок. В последующих статьях мы подробнее рассмотрим добавление дополнительных функций к этому прокси.

Ссылка: Создайте прокси Slack Docker в Go — часть 1 от нашего партнера JCG Йоса Дирксена из блога Smart Java .