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

от автора

Часть 1
Часть 2

После публикации второй статьи, я продолжил изучать библиотеки и развивать свою хотелку с циклом. В итоге начал с варианта добавления атрибута for_each в блок create.

К моему удивлению, рабочее решение родилось в течение часа ?. И для его реализации пришлось обратиться к PartialContent и BodySchema из «Способа 4».

Изучая внутренности метода gohcl.DecodeBody(), я наткнулся на метод получения BodySchema из используемой Go структуры. Для этого используется метод gohcl.ImpliedBodySchema(), который вход принимает interface{}, а на выходе дает hcl.BodySchema.

Таким образом, можно продолжать работать со структурой блока create без создания новой с типом BodySchema.

Итак, актуальный вариант блока create выглядит так:

variables {   developers       = ["jira_user_2", "jira_user_3", "jira_user_4"]   tester           = "jira_user_1"   team_lead        = "jira_user_5"   tech_lead        = "jira_user_5"   release_engineer = "jira_user_6"   services         = [     { name = "service_A", skip = false },     { name = "service_B", skip = true },     { name = "service_C", skip = false },   ] }  create "Task" {   project          = "AG"             # required   # required   summary          = "${iter.name} // Обновить библиотеку Library_A до актуальной версии"   # optional   description      = <<DESC Нужно обновить библиотеку Library_A до актуальной версии. После обновления проверить сервис на regress DESC   app_layer        = "Backend"          # optional   components       = ["${iter.name}"]      # optional   sprint           = 100                # optional   epic             = "AG-6815"          # optional   labels           = ["need-regress"]   # optional   story_point      = 2                  # optional   qa_story_point   = 1                  # optional   assignee         = developers.0       # optional   developer        = developers.0       # optional   team_lead        = team_lead          # optional   tech_lead        = tech_lead          # optional   release_engineer = release_engineer   # optional   tester           = tester             # optional      for_each = [for service in services : service if !service.skip] }

По моей задумке for_each должен иметь итерируемый тип. В процессе итераций в переменных будет доступна переменная с именем iter, в которую будет помещаться текущий элемент итерации.

В моем случае в for_each будет список сервисов, для которых нужно создать задачи. А так как не все сервисы нужно обновлять, то хотелось бы иметь возможность их фильтровать, для этого в структуру добавлен атрибут skip.

В данном примере мы должны получить 2 issue в Jira, где в заголовке и компонентах будут упоминаться service_A и service_C.

&main.CreateBlocks{     Create: {         {             Type:            "Task",             Project:         "AG",             Summary:         "service_A // Обновить библиотеку Library_A до актуальной версии",             Description:     "Нужно обновить библиотеку Library_A до актуальной версии.\nПосле обновления проверить сервис на regress\n",             AppLayer:        "Backend",             Components:      {"service_A"},             SprintId:        100,             Epic:            "AG-6815",             Labels:          {"need-regress"},             StoryPoint:      2,             QaStoryPoint:    1,             Assignee:        "jira_user_2",             Developer:       "jira_user_2",             TeamLead:        "jira_user_5",             TechLead:        "jira_user_5",             ReleaseEngineer: "jira_user_6",             Tester:          "jira_user_1",             Parent:          "",             Remains:         nil,         },         {             Type:            "Task",             Project:         "AG",             Summary:         "service_C // Обновить библиотеку Library_A до актуальной версии",             Description:     "Нужно обновить библиотеку Library_A до актуальной версии.\nПосле обновления проверить сервис на regress\n",             AppLayer:        "Backend",             Components:      {"service_C"},             SprintId:        100,             Epic:            "AG-6815",             Labels:          {"need-regress"},             StoryPoint:      2,             QaStoryPoint:    1,             Assignee:        "jira_user_2",             Developer:       "jira_user_2",             TeamLead:        "jira_user_5",             TechLead:        "jira_user_5",             ReleaseEngineer: "jira_user_6",             Tester:          "jira_user_1",             Parent:          "",             Remains:         nil,         },     }, } 

С конфигом разобрались. Перейдем к коду.

Так как for_each представляет из себя выражение, нам нужно заранее понимать, встречается ли этот атрибут в блоке create или нет, чтобы его заранее «посчитать» и еще наличие переменной iter в блоке тоже приведет к ошибке, так как парсер на момент вызова gohcl.DecodeBody() ничего об этой переменной не знает.

Поэтому задача сводится к следующим шагам:
— найти все блоки create
— проверить наличие for_each
— если атрибут найден, то «просчитать» его и запустить цикл, обрабатывая в каждой итерации текущий блок create и добавляя переменную iter
— если атрибута нет, то использовать привычную функцию gohcl.DecodeBody()

Первым шагом в добавляем в структуру create:

type createConfig struct {   ...   Remains         hcl.Body `hcl:",remain"` }

и дорабатываем код

var createBlock CreateBlocks diags = gohcl.DecodeBody(variablesBlock.Remains, ctx, &createBlock) if diags.HasErrors() { renderDiags(diags, parser.Files())  return nil, diags }

согласно описанному выше алгоритму.

  1. Получаем схему блока create и пытаемся их прочитать

var createBlocks CreateBlocks // p.s. мелкий рефакторинг schema, _ := gohcl.ImpliedBodySchema(&createBlocks) bc, _, diags := variablesBlock.Remains.PartialContent(schema) if diags.HasErrors() { renderDiags(diags, parser.Files())  return nil, diags }

Так как перед этим мы прочитали блок variables, то в variablesBlock.Remains попадет оставшаяся часть Body, которую нам и нужно проанализировать. Для этого мы получаем схему для блока create и пытаемся прочитать ее через PartialContent в том оставшемся куске Body.

Найденные блоки create на выходе из функции вернуться в переменной bc с типом hcl.BodyContent.

  1. Проходимся по найденным блокам create и проверяем наличие for_each

for _, block := range bc.Blocks { var config createConfig         // получаем атрибуты в блоке attr, diags := block.Body.JustAttributes() if diags.HasErrors() { renderDiags(diags, parser.Files())  return nil, diags }          // проверяем, есть ли среди них for_each forEach, found := attr["for_each"]  if found {           ... } else {           ... } }
  1. Если атрибут не найден (попадаем в else).

Вызываем DecodeBody, но в качестве Body передаем уже block.Body. Так как тип issue является лейблом (в структуре объявлен как label), то в block.Body он уже не попадает, так как относится к уровню выше. Поэтому нужно самостоятельно заполнить config.Type.

if found {           ... } else { diags = gohcl.DecodeBody(block.Body, ctx, &config) if diags.HasErrors() { renderDiags(diags, parser.Files())  return nil, diags } config.Type = block.Labels[0]             // очищаем, чтобы не было мусора при выводе в консоль config.Remains = nil             // добавляем очередной блок create в массив блоков createBlocks.Create = append(createBlocks.Create, config) }
  1. Если атрибут найден.

Вычисляем его как выражение.

... if found { var forEachValue cty.Value  diags := gohcl.DecodeExpression(forEach.Expr, ctx, &forEachValue) if diags.HasErrors() { return nil, diags } ...

Так как по моей задумке это итерируемый объект, то проходим в цикле по полученным значениям. Для этого у cty.Value есть метод ForEachElement, который на вход принимает callback функцию для обработки элемента.

            // проходимся в цикле по полученным в for_each значениям             forEachValue.ForEachElement(func(key cty.Value, val cty.Value) (stop bool) {                 // заполняем переменную iter текущим значением                 // в моем случае это объект service с полями name и skip ctx.Variables["iter"] = val                  // передаем Body из блока на парсинг и подстановку переменных diags = gohcl.DecodeBody(block.Body, ctx, &config) if diags.HasErrors() { renderDiags(diags, parser.Files())  return true } config.Type = block.Labels[0]                 // очищаем, чтобы не было мусора при выводе в консоль     config.Remains = nil                 // добавляем очередной блок create в массив блоков     createBlocks.Create = append(createBlocks.Create, config)  return false })

Таким образом окончательные варианты структур выглядят так

type VariablesBlock struct { Variables variables `hcl:"variables,block"` Remains   hcl.Body  `hcl:",remain"` }  type variables struct { Remains hcl.Body `hcl:",remain"` }  type CreateBlocks struct { Create []createConfig `hcl:"create,block"` }  type createConfig struct { Type            string   `hcl:"type,label"` Project         string   `hcl:"project"` Summary         string   `hcl:"summary"` Description     string   `hcl:"description,optional"` AppLayer        string   `hcl:"app_layer,optional"` Components      []string `hcl:"components,optional"` SprintId        int      `hcl:"sprint,optional"` Epic            string   `hcl:"epic,optional"` Labels          []string `hcl:"labels,optional"` StoryPoint      int      `hcl:"story_point,optional"` QaStoryPoint    int      `hcl:"qa_story_point,optional"` Assignee        string   `hcl:"assignee,optional"` Developer       string   `hcl:"developer,optional"` TeamLead        string   `hcl:"team_lead,optional"` TechLead        string   `hcl:"tech_lead,optional"` ReleaseEngineer string   `hcl:"release_engineer,optional"` Tester          string   `hcl:"tester,optional"` Parent          string   `hcl:"parent,optional"` Remains         hcl.Body `hcl:",remain"` }

И функция парсера так

func parse(filename string) (*CreateBlocks, error) { parser := hclparse.NewParser() f, diags := parser.ParseHCLFile(filename) if diags.HasErrors() { renderDiags(diags, parser.Files())  return nil, diags }  ctx := &hcl.EvalContext{ Variables: map[string]cty.Value{}, Functions: map[string]function.Function{ "env": EnvFunc, }, }  var variablesBlock VariablesBlock _ = gohcl.DecodeBody(f.Body, ctx, &variablesBlock)  if variablesBlock.Variables.Remains != nil { variables, diags := variablesBlock.Variables.Remains.JustAttributes() if diags.HasErrors() { renderDiags(diags, parser.Files())  return nil, diags }  for key, variable := range variables { var value cty.Value  diags := gohcl.DecodeExpression(variable.Expr, nil, &value) if diags.HasErrors() { return nil, diags }  ctx.Variables[key] = value } }  var createBlocks CreateBlocks schema, _ := gohcl.ImpliedBodySchema(&createBlocks) bc, _, diags := variablesBlock.Remains.PartialContent(schema) if diags.HasErrors() { renderDiags(diags, parser.Files())  return nil, diags }  for _, block := range bc.Blocks { var config createConfig attr, diags := block.Body.JustAttributes() if diags.HasErrors() { renderDiags(diags, parser.Files())  return nil, diags }  forEach, found := attr["for_each"]  if found { var forEachValue cty.Value  diags := gohcl.DecodeExpression(forEach.Expr, ctx, &forEachValue) if diags.HasErrors() { return nil, diags }  forEachValue.ForEachElement(func(key cty.Value, val cty.Value) (stop bool) { ctx.Variables["iter"] = val  diags = gohcl.DecodeBody(block.Body, ctx, &config) if diags.HasErrors() { renderDiags(diags, parser.Files())  return true } config.Type = block.Labels[0] config.Remains = nil createBlocks.Create = append(createBlocks.Create, config)  return false }) delete(ctx.Variables, "iter") } else { diags = gohcl.DecodeBody(block.Body, ctx, &config) if diags.HasErrors() { renderDiags(diags, parser.Files())  return nil, diags } config.Type = block.Labels[0] config.Remains = nil createBlocks.Create = append(createBlocks.Create, config) } }  return &createBlocks, nil }


ссылка на оригинал статьи https://habr.com/ru/articles/697590/