Задача — написать утилиту, которая будет запускаться по крону из 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:
Готово.
