Вдохновившись статьей «Добавляем в Go-проект конфигурацию на языке Terraform» захотелось попробовать в каком-нибудь проекте описать конфигурацию на HCL.
И как-то, в очередной раз, заменяя переменные в скрипте на python, чтобы создать задачки в Jira, меня посетила мысль, что можно попробовать написать утилитку на Go, которая будет по описанию в HCL генерировать задачи в Jira. Заодно и с Go познакомлюсь.
Забегая вперед скажу, что поиски примеров и изучение парсера дались мне трудно. Кроме пары банальных примеров найти что-то вменяемое мне не удалось. Были мысли сделать на Python, но для Python парсер оказался совсем убогим, мог только перевести HCL в dict и никакой валидации и обработки выражений. Поэтому пришлось вернуться к затее с Go.
Чего хочу добиться от утилиты:
-
возможность создания нескольких задач по описанию в HCL с заполнением разных используемых полей, включая custom fields
-
генерировать сразу несколько задач (бывает нужно сделать 30 похожих задач для разных репозиториев) по шаблону для разных проектов/компонентов
-
линковать задачи, делать подзадачи
-
все то же самое для обновления задач
Итак, начнем с самого простого примера. Хочу описывать структуру создаваемой задачи в блоке create, а для обновления использовать блок update. Начнем пока с create
create "Task" { project = "AA" # required summary = "My first issue" # required assignee = "ivanov" # required description = "issue description" # optional labels = ["no-qa"] # optional }
где Task — это тип issue
Для обработки такого шаблона нам подойдет следующий код
package main import ( "github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/hcl/v2/hclparse" "github.com/kr/pretty" "log" ) type Root struct { Create config `hcl:"create,block"` } type config struct { Type string `hcl:"type,label"` Project string `hcl:"project"` Summary string `hcl:"summary"` Assignee string `hcl:"assignee"` Description string `hcl:"description,optional"` Labels []string `hcl:"labels,optional"` } func main() { filename := "example.hcl" parser := hclparse.NewParser() f, diags := parser.ParseHCLFile(filename) if diags.HasErrors() { log.Fatal(diags) } var root Root diags = gohcl.DecodeBody(f.Body, nil, &root) if diags.HasErrors() { log.Fatal(diags) } _, _ = pretty.Println(root) }
После выполнения кода получаем следующий ответ
main.Root{ Create: main.config{ Type: "Task", Project: "AA", Summary: "My first issue", Assignee: "ivanov", Description: "issue description", Labels: {"no-qa"}, }, }
Пробуем убрать required поле, например, project
2022/11/04 21:55:07 example.hcl:1,15-15: Missing required argument; The argument "project" is required, but no definition was found. exit status 1
Вроде понятно, но можно сделать симпатичнее вывод ошибки. Перед log.Fatal() добавляем следующий код
wr := hcl.NewDiagnosticTextWriter( os.Stdout, // writer to send messages to parser.Files(), // the parser's file cache, for source snippets 78, // wrapping width true, // generate colored/highlighted output ) _ = wr.WriteDiagnostics(diags)
Теперь, если убрать обязательное поле, например, summary, то получим такой вывод
Error: Missing required argument on example.hcl line 1, in create "Task": 1: create "Task" { The argument "summary" is required, but no definition was found. 2022/11/04 21:59:43 example.hcl:1,15-15: Missing required argument; The argument "summary" is required, but no definition was found. exit status 1
Мне такой вариант нравится больше.
Добавляем клиент для Jira и пробуем создать задачу (user/pass для доступа к Jira я добавил в переменные окружения)
package main import ( "fmt" "github.com/andygrunwald/go-jira" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/hcl/v2/hclparse" "github.com/kr/pretty" "log" "os" ) type Root struct { Create config `hcl:"create,block"` } type config struct { Type string `hcl:"type,label"` Project string `hcl:"project"` Summary string `hcl:"summary"` Assignee string `hcl:"assignee"` Description string `hcl:"description,optional"` Labels []string `hcl:"labels,optional"` } func renderDiags(diags hcl.Diagnostics, files map[string]*hcl.File) { wr := hcl.NewDiagnosticTextWriter( os.Stdout, // writer to send messages to files, // the parser's file cache, for source snippets 78, // wrapping width true, // generate colored/highlighted output ) _ = wr.WriteDiagnostics(diags) } func main() { filename := "example.hcl" parser := hclparse.NewParser() f, diags := parser.ParseHCLFile(filename) if diags.HasErrors() { renderDiags(diags, parser.Files()) log.Fatal(diags) } var root Root diags = gohcl.DecodeBody(f.Body, nil, &root) if diags.HasErrors() { renderDiags(diags, parser.Files()) log.Fatal(diags) } _, _ = pretty.Println(root) basicAuth := jira.BasicAuthTransport{ Username: os.Getenv("JIRA_USERNAME"), Password: os.Getenv("JIRA_PASSWORD"), } jiraClient, _ := jira.NewClient(basicAuth.Client(), os.Getenv("JIRA_URL")) i := jira.Issue{ Fields: &jira.IssueFields{ Description: root.Create.Description, Type: jira.IssueType{Name: root.Create.Type}, Project: jira.Project{Key: root.Create.Project}, Summary: root.Create.Summary, Labels: root.Create.Labels, }, } issue, _, err := jiraClient.Issue.Create(&i) if err != nil { log.Fatal(err) } fmt.Println(issue.Key) }
Отлично, простенькую задачку создать удалось
main.Root{ Create: main.config{ Type: "Task", Project: "AA", Summary: "My first issue", Assignee: "ivanov", Description: "issue description", Labels: {"no-qa"}, }, } AA-1234
А что будет, если в файле указать 2 (например с типом Bug) и более блоков create?
Error: Duplicate create block on example.hcl line 9, in create "Bug": 9: create "Bug" { Only one create block is allowed. Another was defined at example.hcl:1,1-14. 2022/11/04 22:20:48 example.hcl:9,1-13: Duplicate create block; Only one create block is allowed. Another was defined at example.hcl:1,1-14. exit status 1
Ок, вроде несложно поправить
... // указываем для Root, что create - это массив type Root struct { Create []config `hcl:"create,block"` } ... // обрабатываем массив из create for _, create := range root.Create { i := jira.Issue{ Fields: &jira.IssueFields{ Description: create.Description, Type: jira.IssueType{Name: create.Type}, Project: jira.Project{Key: create.Project}, Summary: create.Summary, Labels: create.Labels, }, } issue, _, err := jiraClient.Issue.Create(&i) if err != nil { log.Fatal(err) } fmt.Println(issue.Key) } ...
Результат
main.Root{ Create: { { Type: "Task", Project: "AA", Summary: "My first issue", Assignee: "ivanov", Description: "issue description", Labels: {"no-qa"}, }, { Type: "Bug", Project: "AA", Summary: "My first issue", Assignee: "ivanov", Description: "issue description", Labels: {"no-qa"}, }, }, } AA-1001 AA-1002
Уже лучше, но пока этого не достаточно, чтобы заменить мой скрипт на Python.
Для начала я хочу научиться работать с переменными. С этого момента и начались сложности с реализацией моих хотелок из-за отсутствия толковых примеров и документации (либо я плохо искал и не умею пользоваться доками), а так же отсутствия опыта написания программ на Go.
Про добавление переменных и частичный парсинг, я расскажу в следующей части.
Примеры, по ходу приобретения опыта, буду пополнять на github
ссылка на оригинал статьи https://habr.com/ru/post/697500/
Добавить комментарий