Реализация собственного инструмента с помощью HCL (часть 2)
13 апреля 2022 г.Это вторая часть моей серии HCL.
Вы можете найти первую часть [здесь (Часть 1)] (https://blog.weakpixel.com/hate-yaml-build-your-next-tool-with-hcl)
Во втором посте моей серии HCL я хочу расширить наш пример следующим образом:
- Командная строка кобры
- Переменные
- Функции
Кобра
[Cobra] (https://github.com/spf13/cobra) — моя любимая библиотека для создания инструментов командной строки.
Начнем с примера программы из первого поста (источник).
Как я писал ранее, я хочу познакомить вас с инструментом командной строки Cobra. Чтобы использовать его, мы должны добавить новый импорт:
```голанг
импорт (
"ФМТ"
"Операционные системы"
"github.com/spf13/cobra"
Затем переименуйте функцию main()
в newRunCommand()
и реорганизуйте ее, чтобы она возвращала cobra.Command
```голанг
функция newRunCommand() *cobra.Command {
// содержит все переменные, заданные пользователем с параметром --var "key=value"
переменные: = [] строка {}
cmd := cobra.Command{
Использование: «бежать»
Кратко: «Выполняет задачи»,
RunE: func(cmd *cobra.Command, args []string) error {
конфигурация := &Конфигурация{}
err := hclsimple.Decode("example.hcl", []byte(exampleHCL), nil, config)
если ошибка != ноль {
вернуть ошибку
для _, задача := диапазон config.Tasks {
fmt.Printf("Задача: %s
", задача.Имя)
для _, шаг := диапазон задача.Шаги {
fmt.Printf(" Шаг: %s %s
", шаг.Тип, шаг.Имя)
вар бегун бегун
шаг переключения. Тип {
случай "мкдир":
бегун = &MkdirStep{}
случай "exec":
бегун = &ExecStep{}
По умолчанию:
return fmt.Errorf("неизвестный тип шага %q", step.Type)
diags := gohcl.DecodeBody(step.Remain, nil, runner)
если diags.HasErrors() {
возврат диаграмм
ошибка = бегун.Выполнить()
если ошибка != ноль {
вернуть ошибку
вернуть ноль
// Определяем необязательный флаг "var" для команды
cmd.Flags().StringArrayVar(&vars, "var", nil, "Задает переменную. Формат <имя>=<значение>")
вернуть &cmd
Поле «Использовать» описывает имя подкоманды. Поле Short позволяет задать краткое описание команды.
RunE
реализует выполнение (под-)команды. Он содержит наш код разбора HCL. Так как RunE
позволяет нам
возвращать ошибку, мы также переработали код, чтобы он просто возвращал ошибку вместо использования os.Exit(1)
.
После этого мы реализуем новую функцию main
, которая выглядит так:
```голанг
основная функция () {
корень := cobra.Command{
Используйте: "taskexec",
root.AddCommand(newRunCommand())
ошибка := корень.Выполнить()
если ошибка != ноль {
fmt.Println(ошибка)
os.Выход(1)
Корневая команда — это просто пустая cobra.Command
. К корневой команде мы добавляем нашу подкоманду с помощью root.AddCommand(newRunCommand())
.
Давайте попробуем, что произойдет, если мы запустим нашу программу:
иди беги main.go
Использование:
taskexec [команда]
Доступные команды:
завершение Сгенерировать сценарий автозаполнения для указанной оболочки
help Справка по любой команде
run Выполняет задачи
Флаги:
-h, --help справка для taskexec
Попробуем показать справку по подкоманде:
иди беги main.go беги -h
Выполняет задачи
Использование:
taskexec запустить [флаги]
Флаги:
-h, --help помощь для запуска
--var stringArray Устанавливает переменную. Формат <имя>=<значение>
Большой! Далее мы хотим использовать переменные. Чтобы использовать переменные в нашей конфигурации HCL, мы должны узнать о hcl.EvalContext
EvalContext
[hcl.EvalContext] (https://pkg.go.dev/github.com/hashicorp/hcl2/hcl#EvalContext) позволяет определять переменные и функции.
```голанг
введите структуру EvalContext {
Карта переменных[string]cty.Value
Карта функций[строка]function.Function
А пока сосредоточимся на переменных. Карта Variables
позволяет нам определить имя переменной как ключ и как значение cty.Value
. cty.Value
является частью пакета github.com/zclconf/go-cty/cty
. Пакет предоставляет динамическую систему типов.
Вы можете узнать больше о cty
в проекте github.
Вернемся к hcl.EvalContext
. Где на самом деле используется эта контекстная структура? В нашем примере кода у нас есть два экземпляра:
```голанг
hclsimple.Decode("example.hcl", []byte(exampleHCL),
/&hcl.EvalContext{}/ ноль, конфигурация)
и
```голанг
diags := gohcl.DecodeBody(step.Remain,
/&hcl.EvalContext{}/ ноль, бегун)
Переменные
В нашей команде мы определили слайс vars
, который содержит определяемые пользователем переменные в формате:
--var "ключ=значение" ...
Итак, давайте начнем и создадим hcl.EvalContext
и заполним его параметрами vars
из командной строки.
```голанг
func newEvalContext(vars []string) (*hcl.EvalContext, ошибка) {
varMap := карта[строка]cty.Value{}
для _, v := диапазон vars {
эл := strings.Split(v, "=")
если лен(эль) != 2 {
вернуть nil, fmt.Errorf("неверный формат: %s", v)
varMap[el[0]] = cty.StringVal(el[1])
ctx := &hcl.EvalContext{}
ctx.Variables = карта [строка] cty.Value {
"var": cty.ObjectVal(varMap),
вернуть ctx, ноль
Мы используем функцию newEvalContext()
в нашей подкоманде, чтобы создать EvalContext и использовать контекст во всех местах, где мы декодируем документ HCL:
```голанг
RunE: func(cmd *cobra.Command, args []string) error {
ctx, ошибка: = newEvalContext (вары)
если ошибка != ноль {
вернуть ошибку
конфигурация := &Конфигурация{}
err = hclsimple.Decode("example.hcl", []byte(exampleHCL), ctx, config)
для _, задача := диапазон config.Tasks {
fmt.Printf("Задача: %s
", задача.Имя)
для _, шаг := диапазон задача.Шаги {
diags := gohcl.DecodeBody(step.Remain, ctx, бегун)
вернуть ноль
И, наконец, мы меняем наш exampleHCL
, чтобы использовать переменные:
```голанг
примерHCL = `
задача "first_task" {
шаг "mkdir" "build_dir" {
путь = var.builddir
шаг "exec" "list_build_dir" {
command = "ls ${var.buildDir}"
Попробуем выполнить команду без определения переменной buildDir:
иди беги main.иди беги
example.hcl:4,15-24: неподдерживаемый атрибут; У этого объекта нет атрибута с именем "buildDir" и еще 1 диагностики.
статус выхода 1
Хорошо, это терпит неудачу с подробным сообщением об ошибке.
Теперь пробуем выполнить команду с нужной переменной:
go run main.go run --var buildDir=./build
Задача: первая_задача
Шаг: mkdir build_dir
Шаг: выполнить list_build_dir
И это работает так, как ожидалось!
Вы можете увидеть полный исходный код [здесь] (https://gist.github.com/weakpixel/c92f8427b6197a501c1a8d0595e5b5db)
Функции
Далее мы хотим изучить, как, например. Terraform предоставляет эти замечательные встроенные функции, которые значительно облегчают работу с входными переменными.
В нашем примере это может не иметь особого смысла, но давайте попробуем реализовать функцию, которая преобразует все буквы в верхнем регистре:
helloValue = "${верхний ("привет")} мир"
Чтобы реализовать функцию, мы должны добавить новый модуль в наш импорт github.com/zclconf/go-cty/cty/function.
Мы должны использовать структуру function.Spec
, чтобы создать с помощью function.New
нашу реализацию функции:
```голанг
var upperFn = function.New(&function.Spec{
// Определяем необходимые параметры.
Параметры: []функция.Параметр{
Название: "ул",
Тип: cty.String,
Алловдинамический тип: правда,
// Определяем возвращаемый тип
Тип: function.StaticReturnType(cty.String),
// Реализация функции:
Внедрение: func(args []cty.Value, retType cty.Type) (cty.Value, ошибка) {
в := аргументы[0].AsString()
выход: = строки.ToUpper (вход)
вернуть cty.StringVal(out), ноль
И наконец, мы добавляем новую функцию в наш EvalContext:
```голанг
func newEvalContext(vars []string) (*hcl.EvalContext, ошибка) {
ctx.Functions = карта [строка] function.Function {
"верхний": верхнийFn,
вернуть ctx, ноль
Обновите exampleHCL
, чтобы использовать нашу новую функцию определения:
примерHCL = `
задача "first_task" {
шаг "mkdir" "build_dir" {
путь = верхний (var.builddir)
шаг "exec" "list_build_dir" {
command = "ls ${верхний(var.buildDir)}"
Добавьте отладочный вывод в наш пример. Выполнение шага (mkdir, exec) и запустите программу:
go run main.go run --var "buildDir=./build"
Задача: первая_задача
Шаг: mkdir build_dir
Путь:./сборка
Шаг: выполнить list_build_dir
Команда: ls ./СТРОЙКА
и, как и ожидалось, у нас есть каталог сборки в верхнем регистре.
Если вы не хотите реализовывать все функции самостоятельно или вам нужно вдохновение для реализации функции, которую вы хотите найти здесь:
Ресурсы
Ресурсы:
- [Полная суть исходного кода] (https://gist.github.com/weakpixel/40147127fdcac0e11f74967a9ef5aaad)
Полный исходный код
```голанг
основной пакет
импорт (
"ФМТ"
"Операционные системы"
"струны"
"github.com/spf13/cobra"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclsimple"
"github.com/zclconf/go-cty/cty/функция"
вар (
примерHCL = `
задача "first_task" {
шаг "mkdir" "build_dir" {
путь = верхний (var.builddir)
шаг "exec" "list_build_dir" {
command = "ls ${верхний(var.buildDir)}"
основная функция () {
корень := cobra.Command{
Используйте: "taskexec",
root.AddCommand(newRunCommand())
ошибка := корень.Выполнить()
если ошибка != ноль {
fmt.Println(ошибка)
os.Выход(1)
функция newRunCommand() *cobra.Command {
переменные: = [] строка {}
cmd := cobra.Command{
Использование: «бежать»,
Кратко: «Выполняет задачи»,
RunE: func(cmd *cobra.Command, args []string) error {
ctx, ошибка: = newEvalContext (вары)
если ошибка != ноль {
вернуть ошибку
конфигурация := &Конфигурация{}
err = hclsimple.Decode("example.hcl", []byte(exampleHCL), ctx, config)
если ошибка != ноль {
вернуть ошибку
для _, задача := диапазон config.Tasks {
fmt.Printf("Задача: %s
", задача.Имя)
для _, шаг := диапазон задача.Шаги {
fmt.Printf(" Шаг: %s %s
", шаг.Тип, шаг.Имя)
вар бегун бегун
шаг переключения. Тип {
случай "мкдир":
бегун = &MkdirStep{}
случай "exec":
бегун = &ExecStep{}
По умолчанию:
return fmt.Errorf("неизвестный тип шага %q", step.Type)
diags := gohcl.DecodeBody(step.Remain, ctx, бегун)
если diags.HasErrors() {
возврат диаграмм
ошибка = бегун.Выполнить()
если ошибка != ноль {
вернуть ошибку
вернуть ноль
cmd.Flags().StringArrayVar(&vars, "var", nil, "Задает переменную. Формат <имя>=<значение>")
вернуть &cmd
func newEvalContext(vars []string) (*hcl.EvalContext, ошибка) {
varMap := карта[строка]cty.Value{}
для _, v := диапазон vars {
эл := strings.Split(v, "=")
если лен(эль) != 2 {
вернуть nil, fmt.Errorf("неверный формат: %s", v)
varMap[el[0]] = cty.StringVal(el[1])
ctx := &hcl.EvalContext{}
ctx.Variables = карта [строка] cty.Value {
"var": cty.ObjectVal(varMap),
ctx.Functions = карта [строка] function.Function {
"верхний": верхнийFn,
вернуть ctx, ноль
var upperFn = function.New(&function.Spec{
// Определяем необходимые параметры.
Параметры: []функция.Параметр{
Название: "ул",
Тип: cty.String,
Алловдинамический тип: правда,
// Определяем возвращаемый тип
Тип: function.StaticReturnType(cty.String),
// Реализация функции:
Внедрение: func(args []cty.Value, retType cty.Type) (cty.Value, ошибка) {
в := аргументы[0].AsString()
выход: = строки.ToUpper (вход)
вернуть cty.StringVal(out), ноль
введите структуру конфигурации {
Задачи []*Task hcl:"task,block"
введите структуру задачи {
Строка имени hcl:"name,label"
Шаги []*Шаг hcl:"шаг,блок"
тип Шаг структура {
Введите строку hcl:"type,label"
Строка имени hcl:"name,label"
Остаться hcl.Body hcl:",remain"
введите структуру ExecStep {
Командная строка hcl:"команда"
func (s *ExecStep) Ошибка выполнения() {
fmt.Println("\tCommand: " + s.Command)
вернуть ноль
введите структуру MkdirStep {
Строка пути hcl:"путь"
func (s *MkdirStep) Ошибка выполнения() {
fmt.Println("\tPath:" + s.Path)
вернуть ноль
тип интерфейса бегуна {
Ошибка запуска()
Оригинал