Реализация собственного инструмента с помощью HCL (часть 2)

Реализация собственного инструмента с помощью 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)


вернуть ноль


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


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



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