Using the HCL configuration on the example of creating tasks in Jira. Part 1
Inspired by the article “Adding Terraform configuration to a Go-project”, I wanted to try to describe the configuration in HCL in some project.
And somehow, once again, replacing variables in a python script in order to create tasks in Jira, the thought came to me that I could try to write a Go utility that would generate tasks in Jira according to the description in HCL. At the same time, I will get acquainted with Go.
Looking ahead, I will say that the search for examples and the study of the parser were difficult for me. In addition to a couple of banal examples, I could not find something sane. There were thoughts to do it in Python, but for Python, the parser turned out to be completely miserable, it could only translate HCL into a dict and no validation and expression processing. Therefore, I had to return to the idea with Go.
What I want to achieve from the utility:
the ability to create multiple tasks by description in HCL with filling in different fields used, including custom fields
generate multiple tasks at oncesometimes you need to do 30 similar tasks for different repositories) by template for different projects/components
link tasks, make subtasks
all the same for updating tasks
So let’s start with the simplest example. I want to describe the structure of the created task in a block createand to update use the block update. Let’s start with create
create "Task" {
project = "AA" # required
summary = "My first issue" # required
assignee = "ivanov" # required
description = "issue description" # optional
labels = ["no-qa"] # optional
}
where Task is the issue type
To process such a template, the following code will suit us
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)
}
After executing the code, we get the following response
main.Root{
Create: main.config{
Type: "Task",
Project: "AA",
Summary: "My first issue",
Assignee: "ivanov",
Description: "issue description",
Labels: {"no-qa"},
},
}
We try to remove the required field, for example, 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
It seems clear, but you can make the error output prettier. Before log.Fatal() add the following code
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)
Now, if we remove the required field, for example, summarythen we get the following conclusion
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
I like this option better.
We add a client for Jira and try to create a task (I added user / pass to access Jira to the environment variables)
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)
}
Great, I managed to create a simple problem
main.Root{
Create: main.config{
Type: "Task",
Project: "AA",
Summary: "My first issue",
Assignee: "ivanov",
Description: "issue description",
Labels: {"no-qa"},
},
}
AA-1234
And what will happen if you specify 2 (for example, with the Bug type) or more blocks in the file 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
Ok, looks like it’s easy to fix.
...
// указываем для 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)
}
...
Result
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
Already better, but not yet enough to replace my script with Python.
First, I want to learn how to work with variables. From that moment, difficulties began with the implementation of my Wishlist due to the lack of sensible examples and documentation (or I didn’t search well and I don’t know how to use the docks), as well as the lack of experience in writing programs in Go.
I will talk about adding variables and partial parsing in the next part.
Examples, in the course of gaining experience, I will replenish on github