Как реализовать свой собственный инструмент с помощью 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) Ошибка выполнения() {
// Реализуй меня
вернуть ноль
тип интерфейса бегуна {
Ошибка запуска()
Оригинал