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

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *