Использование HCL-конфигурации на примере создания задач в Jira. Часть 1

от автора

Вдохновившись статьей «Добавляем в 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *