Задача – написать утилиту, которая будет запускаться по крону из Jenkins и проверять список публичных репозиториев организации в Github.
Создание Docker-образа и Jenkins-джобы – в посте Jenkins: проверка публичных репозиториев Github-организации.
Затем она должна сравнивать полученный список со списком разрешённых, и если списки не совпадают – слать алерт в Slack.
Идея состоит в том, что если девелоперы случайно создадут новый публичный репозиторий, или сменят тип существующего с приватного на публичный – получить об этом уведомление.
Можно было бы написать на bash
с curl
, или на Python с urllib
и использовать Github API напрямую, но практики ради – хочется попробовать на Go.
По сути – это моя первая программа на Go, на котором я особо никогда не писал и даже ситаксиса толком не знаю, но попробуем использовать имеющиеся знания C/Python, приправить небольшой порцией логики и, конечно же с помощью Goolge и такой-то матери – написать что-то рабочее.
Документация – для ленивых, поэтому по большей части используем метод тыка (и ещё раз Google).
Для работы с API Github используем пакет go-github
.
Для тестов создал организацию в Github, и два публичных репозитория.
Начинаем писать.
Добавляем импорт, функция подставилась в vim
автоматом плагином vim-go
:
package main import ( "fmt" "github.com/google/go-github/github" ) func main() { fmt.Println("vim-go") }
Добавим кастомный $GOPATH
:
[simterm]
$ sudo mkdir /usr/local/go $ sudo chown setevoy:setevoy /usr/local/go/ $ export GOPATH=/usr/local/go && export GOBIN=/usr/local/go/bin
[/simterm]
Устанавливаем пакет:
[simterm]
$ go get # _/home/setevoy/Scripts/Go/GTHB ./gitrepos.go:5:2: imported and not used: "github.com/google/go-github/github"
[/simterm]
Окей.
Содержание
Получение репозиториев Github
Копипастим первый пример из примеров в README пакета, имя организации получаем из переменной $GITHUB_ORG_NAME
, и попробуем вывести результат.
Задаём переменную:
[simterm]
$ export GITHUB_ORG_NAME="rtfmorg"
[/simterm]
Код:
package main import ( "context" "fmt" "github.com/google/go-github/github" ) func main() { client := github.NewClient(nil) opt := &github.RepositoryListByOrgOptions{Type: "public"} repos, _, _ := client.Repositories.ListByOrg(context.Background(), os.Getenv("GITHUB_ORG_NAME"), opt) fmt.Printf(repos) }
В оригинале имя организации задаётся прямо в коде, мы выносим его в переменную $GITHUB_ORG_NAME
, что бы передавать из Jenkins, и получаем через os.Getenv
.
Добавляем импорт context
и os
.
Запускаем:
[simterm]
$ go run go-github-public-repos-checker.go # command-line-arguments ./go-github-public-repos-checker.go:17:15: cannot use repos (type []*github.Repository) as type string in argument to fmt.Printf
[/simterm]
Ммм…
Окей.
Я думал client.Repositories.ListByOrg
вернёт просто список – List ведь 🙂
Проверим – что за тип получаем в объекте repos
. Используем reflect
:
package main import ( "os" "context" "fmt" "reflect" "github.com/google/go-github/github" ) func main() { client := github.NewClient(nil) opt := &github.RepositoryListByOrgOptions{Type: "public"} repos, _, _ := client.Repositories.ListByOrg(context.Background(), os.Getenv("GITHUB_ORG_NAME"), opt) fmt.Println(reflect.TypeOf(repos).String()) }
Запускаем:
[simterm]
$ go run go-github-public-repos-checker.go []*github.Repository
[/simterm]
Хорошо… Всё-таки список ([]
), указывающий на, судя по всему, структуру – github.Repository
. См. Golang: struct — структуры в примерах.
Посмотрим, что вернёт go doc
:
[simterm]
$ go doc github.Repository type Repository struct { ID *int64 `json:"id,omitempty"` NodeID *string `json:"node_id,omitempty"` Owner *User `json:"owner,omitempty"` Name *string `json:"name,omitempty"` FullName *string `json:"full_name,omitempty"` Description *string `json:"description,omitempty"` ...
[/simterm]
Таки структура, и заодно – всё её поля.
Что бы Go не ругался с сообщением imported and not used: “reflect” на неиспользованный reflext
– задаём его как _reflect
:
package main import ( "os" "context" "fmt" _"reflect" "github.com/google/go-github/github" ) func main() { client := github.NewClient(nil) opt := &github.RepositoryListByOrgOptions{Type: "public"} repos, _, _ := client.Repositories.ListByOrg(context.Background(), os.Getenv("GITHUB_ORG_NAME"), opt) for _, repo := range repos { fmt.Printf("Repo: %s\n", *repo.Name) } }
Тут из списка repos
получаем индекс элемента, и его значение.
Можно вывести и сами индексы:
... for id, repo := range repos { fmt.Printf("%d Repo: %s\n", id, *repo.Name) } ...
[simterm]
$ go run go-github-public-repos-checker.go 0 Repo: org-repo-1-pub 1 Repo: org-repo-2-pub
[/simterm]
Сравнение списков в Go
Теперь надо добавить список, в который будем получать список разрешённых репозиториев.
Разрешённые будем передавать из параметров Jenkins, в виде списка с элементами, разделёнными пробелами:
[simterm]
$ export ALLOWED_REPOS="1 2"
[/simterm]
Попытка номер раз
Добавляем получение списка разрешённых репозиториев, и сохраняем его в список allowedRepos
типа string
:
... allowedRepos := []string{os.Getenv("ALLOWED_REPOS")} for id, repo := range repos { fmt.Printf("%d Repo: %s\n", id, *repo.Name) } fmt.Printf("Allowed repos: %s\n", allowedRepos) ...
Но – есть нюанс, с которым я столкнулся уже позже, когда начал выполнять сравнение списков, и о котором не подумал сразу.
В чём проблема – увидим дальше, пока оставим, как есть.
Проверяем:
[simterm]
$ go run go-github-public-repos-checker.go 0 Repo: org-repo-1-pub 1 Repo: org-repo-2-pub Allowed repos: [1 2]
[/simterm]
Окей, работает.
Теперь надо выполнить проверку между элементами двух списков – repos
и allowedRepos
.
Может есть более красивые решения, но у меня сначала получилось такое:
... allowedRepos := []string{os.Getenv("ALLOWED_REPOS")} fmt.Printf("Allowed repos: %s\n", allowedRepos) for id, repo := range repos { for _, i := range allowedRepos { if i == *repo.Name { fmt.Printf("Index: %d, repo %s found in Allowed as %s\n", id, *repo.Name, i) } else { fmt.Printf("ALARM: repo %s was NOT found in Allowed!\n", *repo.Name) } } } ...
Запускаем:
[simterm]
$ go run go-github-public-repos-checker.go Allowed repos: [1 2] ALARM: repo org-repo-1-pub was NOT found in Allowed! ALARM: repo org-repo-2-pub was NOT found in Allowed!
[/simterm]
Казалось бы – всё верно?
Репозитории с именами 1 и 2 != org-repo-1-pub и org-repo-2-pub.
Проблема, наверняка кому-то уже очевидная, обнаружилась, когда я выполнил обратную проверку, т.е. задал в $ALLOWED_REPOS
реальные имена, что бы получить “ОК”:
[simterm]
$ export ALLOWED_REPOS="org-repo-1-pub org-repo-2-pub"
[/simterm]
Проверяем:
[simterm]
$ go run go-github-public-repos-checker.go Allowed repos: [org-repo-1-pub org-repo-2-pub] ALARM: repo org-repo-1-pub was NOT found in Allowed! ALARM: repo org-repo-2-pub was NOT found in Allowed!
[/simterm]
Пачиму?
А потому, что в allowedRepos
у нас не список aka slice в Go, а просто строка, string.
Добавим вывод значения самой переменной i
, и номера индексов:
... for r_id, repo := range repos { for a_id, i := range allowedRepos { fmt.Printf("ID: %d Type: %T Value: %s\n", a_id, allowedRepos, i) if i == *repo.Name { fmt.Printf("Index: %d, repo %s found in Allowed as %s\n", r_id, *repo.Name, i) } else { fmt.Printf("ALARM: repo %s was NOT found in Allowed!\n", *repo.Name) } } } ...
Проверяем:
[simterm]
$ go run go-github-public-repos-checker.go Allowed repos: [org-repo-1-pub org-repo-2-pub] ID: 0 Type: []string Value: org-repo-1-pub org-repo-2-pub ALARM: repo org-repo-1-pub was NOT found in Allowed! ID: 0 Type: []string Value: org-repo-1-pub org-repo-2-pub ALARM: repo org-repo-2-pub was NOT found in Allowed!
[/simterm]
allowedRepos
у нас таки список, но у него только один элемент с ID 0, в котором хранится значение “org-repo-1-pub org-repo-2-pub“.
Попытка номер два
Что бы наша схема сработала – надо превратить его в настоящий список.
Используем strings
:
... // allowedRepos := []string{os.Getenv("ALLOWED_REPOS")} allowedRepos := strings.Fields(os.Getenv("ALLOWED_REPOS")) fmt.Printf("Allowed repos: %s\n", allowedRepos) for r_id, repo := range repos { for a_id, i := range allowedRepos { fmt.Printf("ID: %d Type: %T Value: %s\n", a_id, allowedRepos, i) if i == *repo.Name { fmt.Printf("Index: %d, repo %s found in Allowed as %s\n", r_id, *repo.Name, i) } else { fmt.Printf("ALARM: repo %s was NOT found in Allowed!\n", *repo.Name) } } } ...
allowedRepos
теперь создаём с помощью strings.Fields()
.
Проверяем результат:
[simterm]
$ go run go-github-public-repos-checker.go Allowed repos: [org-repo-1-pub org-repo-2-pub] ID: 0 Type: []string Value: org-repo-1-pub Index: 0, repo org-repo-1-pub found in Allowed as org-repo-1-pub ID: 1 Type: []string Value: org-repo-2-pub ALARM: repo org-repo-1-pub was NOT found in Allowed! ID: 0 Type: []string Value: org-repo-1-pub ALARM: repo org-repo-2-pub was NOT found in Allowed! ID: 1 Type: []string Value: org-repo-2-pub Index: 1, repo org-repo-2-pub found in Allowed as org-repo-2-pub
[/simterm]
Уже лучше, но такой подход тоже не работает – есть “ложные” срабатывания, т.к. сравнивается каждый элемент repos
с каждым элементом allowedRepos
.
Попытка номер три
Перепишем, и используем перебор по индексам:
... for r_id, repo := range repos { fmt.Printf("%d %s\n", r_id, *repo.Name) if *repo.Name != allowedRepos[r_id] { fmt.Printf("ALARM: repo %s was NOT found in Allowed!\n", *repo.Name) } else { fmt.Printf("Repo %s found in Allowed\n", *repo.Name) } } ...
Т.е. из repos
получаем id индекса, а потом проверяем allowedRepos
с таким же индексом.
Проверяем:
[simterm]
$ go run go-github-public-repos-checker.go Allowed repos: [org-repo-1-pub org-repo-2-pub] 0 org-repo-1-pub Repo org-repo-1-pub found in Allowed 1 org-repo-2-pub Repo org-repo-2-pub found in Allowed
[/simterm]
Отлично!
Но снова проблема.
Что, если порядок репозиториев в обоих списках будет различаться?
Задаём вместо:
[simterm]
$ export ALLOWED_REPOS="org-repo-1-pub org-repo-2-pub"
[/simterm]
Имена репозиториев в обратном порядке:
[simterm]
$ export ALLOWED_REPOS="org-repo-2-pub org-repo-1-pub"
[/simterm]
Проверяем:
[simterm]
$ go run go-github-public-repos-checker.go Allowed repos: [org-repo-2-pub org-repo-1-pub] 0 org-repo-1-pub ALARM: repo org-repo-1-pub was NOT found in Allowed! 1 org-repo-2-pub ALARM: repo org-repo-2-pub was NOT found in Allowed!
[/simterm]
Кек…
Попытка номер четыре
Следующей попыткой было использовать reflect
и DeepEqual
.
Добавляем ещё один список actualRepos
, в который через append
добавляем репозитории, полученные из Github, а потом сравниваем списки:
... allowedRepos := strings.Fields(os.Getenv("ALLOWED_REPOS")) var actualRepos []string for _, repo := range repos { actualRepos = append(actualRepos, *repo.Name) } fmt.Printf("Allowed: %s\n", allowedRepos) fmt.Printf("Actual: %s\n", actualRepos) fmt.Println("Slice equal: ", reflect.DeepEqual(allowedRepos, actualRepos)) ...
Проверяем:
[simterm]
$ go run go-github-public-repos-checker.go Allowed: [org-repo-2-pub org-repo-1-pub] Actual: [org-repo-1-pub org-repo-2-pub] Slice equal: false
[/simterm]
И снова нет… Хотя если вернуть одинаковый порядок – то этот метод сработает:
[simterm]
$ export ALLOWED_REPOS="org-repo-1-pub org-repo-2-pub" $ go run go-github-public-repos-checker.go Allowed: [org-repo-1-pub org-repo-2-pub] Actual: [org-repo-1-pub org-repo-2-pub] Slice equal: true
[/simterm]
Значит – надо вернуться к первому варианту с циклом, но каким-то образом проверять наличие repo
во всём списке allowedRepos
, и только при остутствии repo
во всех элементах списка allowedRepos
– создавать алерт.
Попытка номер пять
Решение нарисовалось следующее: создадим отдельную функцию, которая будет проверять в цикле все элементы allowedRepos
, и если элемент найден – возвращать true
, и прерывать выполнение. В противном случае, по окончании цикла – вернём false
, и сможем его обработать в цикле функции main()
.
Пробуем.
Создаём функцию isAllowedRepo()
, которая будет принимать имя репозитория для проверки, и список разрешённых репозиториев, и возвращать булево значение:
... func isAllowedRepo(repoName string, allowedRepos []string) bool { for _, i := range allowedRepos { if i == repoName { return true } } return false } ...
А в main()
– в цикле перебираем элементы списка repos
, и по одному передаём их в isAllowedRepo()
, и проверяем полученный результат:
... for _, repo := range repos { fmt.Printf("Checking %s\n", *repo.Name) fmt.Println(isAllowedRepo(*repo.Name, allowedRepos)) } ...
Проверяем.
Сначала список с репозиториями в том же порядке:
[simterm]
$ export ALLOWED_REPOS="org-repo-1-pub org-repo-2-pub" $ go run go-github-public-repos-checker.go Checking org-repo-1-pub true Checking org-repo-2-pub true
[/simterm]
И в обратном порядке:
[simterm]
$ export ALLOWED_REPOS="org-repo-2-pub org-repo-1-pub" $ go run go-github-public-repos-checker.go Checking org-repo-1-pub true Checking org-repo-2-pub true
[/simterm]
И уберём один репозиторий из списка разрешённых:
[simterm]
$ export ALLOWED_REPOS="org-repo-2-pub" $ go run go-github-public-repos-checker.go Checking org-repo-1-pub false Checking org-repo-2-pub true
[/simterm]
Отлично.
Теперь при получении false
– можем создавать алерт.
Весь код сейчас выглядит так, оставляю старые попытки “для истории”:
package main import ( "os" "context" "fmt" _"reflect" "strings" "github.com/google/go-github/github" ) func isAllowedRepo(repoName string, allowedRepos []string) bool { for _, i := range allowedRepos { if i == repoName { return true } } return false } func main() { client := github.NewClient(nil) opt := &github.RepositoryListByOrgOptions{Type: "public"} repos, _, _ := client.Repositories.ListByOrg(context.Background(), os.Getenv("GITHUB_ORG_NAME"), opt) // allowedRepos := []string{os.Getenv("ALLOWED_REPOS")} allowedRepos := strings.Fields(os.Getenv("ALLOWED_REPOS")) // var actualRepos []string /* for _, repo := range repos { actualRepos = append(actualRepos, *repo.Name) } fmt.Printf("Allowed: %s\n", allowedRepos) fmt.Printf("Actual: %s\n", actualRepos) fmt.Println("Slice equal: ", reflect.DeepEqual(allowedRepos, actualRepos)) */ for _, repo := range repos { fmt.Printf("Checking %s\n", *repo.Name) fmt.Println(isAllowedRepo(*repo.Name, allowedRepos)) } /* for r_id, repo := range repos { for _, i := range allowedRepos { fmt.Printf("Checking %s and %s\n", *repo.Name, i) if i == *repo.Name { fmt.Printf("Index: %d, repo %s found in Allowed as %s\n", r_id, *repo.Name, i) break } else { fmt.Printf("ALARM: repo %s was NOT found in Allowed!\n", *repo.Name) } } } */ /* for r_id, repo := range repos { fmt.Printf("%d %s\n", r_id, *repo.Name) if *repo.Name != allowedRepos[r_id] { fmt.Printf("ALARM: repo %s was NOT found in Allowed!\n", *repo.Name) } else { fmt.Printf("Repo %s found in Allowed\n", *repo.Name) } } */ /* for r_id, repo := range repos { for a_id, i := range allowedRepos { fmt.Printf("ID: %d Type: %T Value: %s\n", a_id, allowedRepos, i) if i == *repo.Name { fmt.Printf("Index: %d, repo %s found in Allowed as %s\n", r_id, *repo.Name, i) } else { fmt.Printf("ALARM: repo %s was NOT found in Allowed!\n", *repo.Name) } } } */ }
Golang Slack
Добавим отправку уведомления в Slack.
Используем ashwanthkumar/slack-go-webhook
.
Настраиваем вебхук, см. пример в Nagios: настройка уведомлений в Slack-чат.
Добавляем функцию sendSlackAlarm()
, которой тоже передаём два аргумента – имя репозитория, и его URL:
... func sendSlackAlarm(repoName string, repoUrl string) { webhookUrl := os.Getenv("SLACK_URL") text := fmt.Sprintf(":scream: ALARM: repository *%s* was NOT found in Allowed!", repoName) attachment := slack.Attachment{} attachment.AddAction(slack.Action{Type: "button", Text: "RepoURL", Url: repoUrl, Style: "danger"}) payload := slack.Payload{ Username: "Github checker", Text: text, Channel: os.Getenv("SLACK_CHANNEL"), IconEmoji: ":scream:", Attachments: []slack.Attachment{attachment}, } err := slack.Send(webhookUrl, "", payload) if len(err) > 0 { fmt.Printf("error: %s\n", err) } } ...
И вызов в main()
, если isAllowedRepo()
вернул false
:
... for _, repo := range repos { fmt.Printf("\nChecking %s\n", *repo.Name) if isAllowedRepo(*repo.Name, allowedRepos) { fmt.Printf("OK: repo %s found in Allowed\n", *repo.Name) } else { fmt.Printf("ALARM: repo %s was NOT found in Allowed!\n", *repo.Name) sendSlackAlarm(*repo.Name, *repo.HTMLURL) } } ...
repo.HTMLURL
мы нашли в списке полей при вызове go doc github.Repository
.
Задаём $SLACK_URL
и $SLACK_CHANNEL
:
[simterm]
$ export SLACK_URL="https://hooks.slack.com/services/T1641GRB9/BA***WRE" $ export SLACK_CHANNEL="#general"
[/simterm]
Возвращаем полный список репозиториев, в обратном порядке:
[simterm]
$ export ALLOWED_REPOS="org-repo-2-pub org-repo-1-pub"
[/simterm]
Проверяем:
[simterm]
$ go run go-github-public-repos-checker.go Checking org-repo-1-pub OK: repo org-repo-1-pub found in Allowed Checking org-repo-2-pub OK: repo org-repo-2-pub found in Allowed
[/simterm]
Убираем один из разрешённых:
[simterm]
$ export ALLOWED_REPOS="org-repo-2-pub"
[/simterm]
Проверяем ещё раз:
[simterm]
$ go run go-github-public-repos-checker.go Checking org-repo-1-pub ALARM: repo org-repo-1-pub was NOT found in Allowed! Checking org-repo-2-pub OK: repo org-repo-2-pub found in Allowed
[/simterm]
Slack:
Готово.