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