Как реализовать свой собственный инструмент с помощью HCL (потому что я ненавижу YAML)

Как реализовать свой собственный инструмент с помощью HCL (потому что я ненавижу YAML)

9 апреля 2022 г.

В этом посте я хочу показать, как вы можете реализовать свой собственный инструмент, используя формат HCL.


Формат конфигурации HCL используется всеми замечательными инструментами HasiCorp, такими как Terraform, Vault и Nomad.


В настоящее время большинство современных приложений и инструментов используют YAML, который, как правило, легко читается, но также может быть причиной большой боли из-за чувствительности к пробелам. HCL, с другой стороны, предоставляет удобный для чтения формат с четкой структурой и дополнительными функциями, такими как интерполяция переменных и встроенные вызовы функций.


Давайте начнем с действительно простого примера разбора HCL.


задача "first_task" {


exec "list_current_dir" {


команда = "лс."


exec "list_var_dir" {


команда = "лс/вар"


Чтобы сопоставить HCL с нашими структурами, мы можем использовать теги структуры:


```иди


введите структуру конфигурации {


Задачи []*Task hcl:"task,block"


введите структуру задачи {


Строка имени hcl:"name,label"


Шаги []*ExecStep hcl:"exec,block"


введите структуру ExecStep {


Строка имени hcl:"name,label"


Командная строка hcl:"команда"


func (s *ExecStep) Ошибка выполнения() {


вернуть ноль


А для декодирования HCL мы можем использовать функцию decode из пакета hclsimple


```иди


импорт (


"ФМТ"


"Операционные системы"


"github.com/hashicorp/hcl/v2/hclsimple"


вар (


примерHCL = `


задача "first_task" {


exec "list_current_dir" {


команда = "лс."


exec "list_var_dir" {


команда = "лс/вар"


основная функция () {


конфигурация := &Конфигурация{}


err := hclsimple.Decode("example.hcl", []byte(exampleHCL), nil, config)


если ошибка != ноль {


fmt.Println(ошибка)


os.Выход(1)


для _, задача := диапазон config.Tasks {


fmt.Printf("Задача: %s
", задача.Имя)


для _ шаг := диапазон задача.Шаги {


fmt.Printf(" Шаг: %s %q
", шаг.Имя, шаг.Команда)


ошибка = шаг.Выполнить()


если ошибка != ноль {


fmt.Println(ошибка)


os.Выход(1)


Это было просто!


Но что, если я хочу поддерживать другой тип шага? Допустим, я хочу поддерживать mkdir, чтобы легко создавать каталоги.


задача "first_task" {


mkdir "build_dir" {


путь = "./сборка"


exec "list_var_dir" {


команда = "лс/вар"


Если я запускаю наш инструмент, я получаю следующую ошибку:


example.hcl:3,4-9: неподдерживаемый тип блока; Блоки типа "mkdir" здесь не ожидаются.


Мы могли бы обновить нашу структуру Task и добавить черный список для «mkdir»:


```иди


введите структуру задачи {


Строка имени hcl:"name,label"


ExecSteps []*ExecStep hcl:"exec,block"


MkdirStep []*MkdirStep hcl:"mkdir,block"


но очевидно, что мы потеряли бы порядок выполнения, так как у нас есть два отдельных списка. Это не сработает для нас.


В качестве альтернативного решения мы могли бы изменить нашу конфигурацию и сделать Step type меткой:


задача "first_task" {


шаг "mkdir" "build_dir" {


путь = "./сборка/"


шаг "exec" "list_build_dir" {


команда = "ls ./сборка/"


Давайте отразим изменение конфигурации в наших структурах.


```иди


введите структуру конфигурации {


Задачи []*Task hcl:"task,block"


введите структуру задачи {


Строка имени hcl:"name,label"


Шаги []*Шаг hcl:"шаг,блок"


тип Шаг структура {


Введите строку hcl:"type,label"


Строка имени hcl:"name,label"


Остаться hcl.Body hcl:",remain"


введите структуру ExecStep {


Командная строка hcl:"команда"


func (s *ExecStep) Ошибка выполнения() {


вернуть ноль


введите структуру MkdirStep {


Строка пути hcl:"путь"


func (s *MkdirStep) Ошибка выполнения() {


вернуть ноль


Как видите, мы добавили новую структуру Step и используем ее в структуре Tasks вместо ExecStep.


Структура Step имеет дополнительное поле под названием Remain. Это поле необходимо для захвата всех полей фактической реализации Step. Позже мы увидим, как мы используем поле «Remain» для декодирования полей в нашу реальную реализацию Step.


Наконец, мы добавляем новый интерфейс, который позволяет нам запускать реализацию Step:


```иди


тип интерфейса бегуна {


Ошибка запуска()


Вы можете видеть выше, что все наши шаги реализуют интерфейс Runner.


Теперь мы должны адаптировать наш синтаксический анализ кода:


```иди


импорт (


"ФМТ"


"Операционные системы"


"github.com/hashicorp/hcl/v2"


"github.com/hashicorp/hcl/v2/gohcl"


"github.com/hashicorp/hcl/v2/hclsimple"


вар (


примерHCL = `


задача "first_task" {


шаг "mkdir" "build_dir" {


путь = "./сборка/"


шаг "exec" "list_build_dir" {


команда = "ls ./сборка/"


основная функция () {


конфигурация := &Конфигурация{}


err := hclsimple.Decode("example.hcl", []byte(exampleHCL), nil, config)


если ошибка != ноль {


fmt.Println(ошибка)


os.Выход(1)


для _, задача := диапазон config.Tasks {


fmt.Printf("Задача: %s
", задача.Имя)


для _, шаг := диапазон задача.Шаги {


fmt.Printf(" Шаг: %s %s
", шаг.Тип, шаг.Имя)


// Фактическая реализация шага


// который реализует интерфейс Runner


вар бегун бегун


// Определяем реализацию шага


шаг переключения. Тип {


случай "мкдир":


бегун = &MkdirStep{}


случай "exec":


бегун = &ExecStep{}


По умолчанию:


fmt.Printf("Неизвестный тип шага %q
", step.Type)


os.Выход(1)


// Декодируем оставшиеся поля в нашу пошаговую реализацию.


diags := gohcl.DecodeBody(step.Remain, nil, runner)


если diags.HasErrors() {


fmt.Println(diags)


os.Выход(1)


ошибка = бегун.Выполнить()


если ошибка != ноль {


fmt.Println(ошибка)


os.Выход(1)


Синтаксический анализ определяет тип Step в операторе switch и создает экземпляр Struct. Затем структура становится целью gohcl.DecodeBody(step.Remain, nil, runner), которая декодирует оставшиеся поля.


Вуаля, у нас есть легко расширяемый механизм выполнения задач.


Ресурсы


Годоки:


  • [hcl] (https://pkg.go.dev/github.com/hashicorp/hcl/v2)

  • [hclsimple] (https://pkg.go.dev/github.com/hashicorp/hcl/v2/hclsimple)

  • [gohcl] (https://pkg.go.dev/github.com/hashicorp/hcl/v2/gohcl)

Другие:



Исходники:


  • [Пример 1] (https://gist.github.com/weakpixel/8501a0dabeeb800ef38b6d1cc9fa1e72)

  • [Пример 2] (https://gist.github.com/weakpixel/356f44e8a3d0a74c6e542967ade5eb00)

Полный исходный код


```иди


основной пакет


импорт (


"ФМТ"


"Операционные системы"


"github.com/hashicorp/hcl/v2"


"github.com/hashicorp/hcl/v2/gohcl"


"github.com/hashicorp/hcl/v2/hclsimple"


вар (


примерHCL = `


задача "first_task" {


шаг "mkdir" "build_dir" {


путь = "./сборка/"


шаг "exec" "list_build_dir" {


команда = "ls ./сборка/"


основная функция () {


конфигурация := &Конфигурация{}


err := hclsimple.Decode("example.hcl", []byte(exampleHCL), nil, config)


если ошибка != ноль {


fmt.Println(ошибка)


os.Выход(1)


для _, задача := диапазон config.Tasks {


fmt.Printf("Задача: %s
", задача.Имя)


для _, шаг := диапазон задача.Шаги {


fmt.Printf(" Шаг: %s %s
", шаг.Тип, шаг.Имя)


вар бегун бегун


шаг переключения. Тип {


случай "мкдир":


бегун = &MkdirStep{}


случай "exec":


бегун = &ExecStep{}


По умолчанию:


fmt.Printf("Неизвестный тип шага %q
", step.Type)


os.Выход(1)


diags := gohcl.DecodeBody(step.Remain, nil, runner)


если diags.HasErrors() {


fmt.Println(diags)


os.Выход(1)


ошибка = бегун.Выполнить()


если ошибка != ноль {


fmt.Println(ошибка)


os.Выход(1)


введите структуру конфигурации {


Задачи []*Task hcl:"task,block"


введите структуру задачи {


Строка имени hcl:"name,label"


Шаги []*Шаг hcl:"шаг,блок"


тип Шаг структура {


Введите строку hcl:"type,label"


Строка имени hcl:"name,label"


Остаться hcl.Body hcl:",remain"


введите структуру ExecStep {


Командная строка hcl:"команда"


func (s *ExecStep) Ошибка выполнения() {


// Реализуй меня


вернуть ноль


введите структуру MkdirStep {


Строка пути hcl:"путь"


func (s *MkdirStep) Ошибка выполнения() {


// Реализуй меня


вернуть ноль


тип интерфейса бегуна {


Ошибка запуска()



Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE