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

Продолжение

Комментарии (0)