From fd9c38aba51b4716be1ccfde2ae9044143c262a4 Mon Sep 17 00:00:00 2001 From: Mikael Olenfalk Date: Mon, 17 Apr 2017 20:07:18 +0200 Subject: [PATCH] First initial release --- .gitignore | 1 + README.md | 7 ++ index/index.go | 207 +++++++++++++++++++++++++++++++++++++++++++++ terraform-ast.go | 147 -------------------------------- terraform-index.go | 61 +++++++++++++ 5 files changed, 276 insertions(+), 147 deletions(-) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 index/index.go delete mode 100644 terraform-ast.go create mode 100644 terraform-index.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6ab6700 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +terraform-index diff --git a/README.md b/README.md new file mode 100644 index 0000000..fffec23 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# README.md + +A simple tool which prints the AST of a HCL file (e.g. Terraform file) as well +as extracts the position of some interesting declarations like variabels and +resource declarations. + +Primarily created for use by https://github.com/mauve/vscode-terraform \ No newline at end of file diff --git a/index/index.go b/index/index.go new file mode 100644 index 0000000..39d8d0f --- /dev/null +++ b/index/index.go @@ -0,0 +1,207 @@ +package index + +import ( + "regexp" + "strings" + + "github.com/hashicorp/hcl" + "github.com/hashicorp/hcl/hcl/ast" + "github.com/hashicorp/hcl/hcl/parser" + "github.com/hashicorp/hcl/hcl/token" +) + +type VariableDeclaration struct { + Name string + Location token.Pos +} + +type ResourceDeclaration struct { + Type string + Name string + Location token.Pos +} + +type OutputDeclaration struct { + Name string + Location token.Pos +} + +type ReferenceList struct { + Name string + Locations []token.Pos +} + +type Error struct { + Message string + Location token.Pos +} + +type Index struct { + Errors []Error + Variables []VariableDeclaration + Resources []ResourceDeclaration + Outputs []OutputDeclaration + References map[string]ReferenceList + RawAst *ast.File +} + +func NewIndex() *Index { + index := new(Index) + index.Errors = []Error{} + index.Variables = []VariableDeclaration{} + index.Resources = []ResourceDeclaration{} + index.Outputs = []OutputDeclaration{} + index.References = map[string]ReferenceList{} + index.RawAst = nil + return index +} + +func (index *Index) Collect(astFile *ast.File, path string, includeRaw bool) error { + ast.Walk(astFile.Node, func(current ast.Node) (ast.Node, bool) { + switch current.(type) { + case *ast.ObjectList: + { + index.handleObjectList(current.(*ast.ObjectList), path) + break + } + + case *ast.LiteralType: + { + index.handleLiteral(current.(*ast.LiteralType), path) + break + } + } + + return current, true + }) + + if includeRaw { + index.RawAst = astFile + } + return nil +} + +func (index *Index) CollectString(contents []byte, path string, includeRaw bool) error { + astFile, err := hcl.ParseBytes(contents) + if err != nil { + index.Errors = append(index.Errors, makeError(err, path)) + return err + } + + return index.Collect(astFile, path, includeRaw) +} + +func makeError(err error, path string) Error { + if posError, ok := err.(*parser.PosError); ok { + return Error{ + Message: posError.Err.Error(), + Location: posError.Pos, + } + } + + return Error{ + Message: err.Error(), + } +} + +func getText(t token.Token) string { + return strings.Trim(t.Text, "\"") +} + +func getPos(t token.Token, path string) token.Pos { + location := t.Pos + location.Filename = path + return location +} + +func (index *Index) handleObjectList(objectList *ast.ObjectList, path string) { + for _, item := range objectList.Items { + firstToken := item.Keys[0].Token + if firstToken.Type != 4 { + continue + } + + switch firstToken.Text { + case "variable": + { + variable := VariableDeclaration{ + Name: getText(item.Keys[1].Token), + Location: getPos(item.Keys[1].Token, path), + } + index.Variables = append(index.Variables, variable) + break + } + + case "resource": + { + resource := ResourceDeclaration{ + Name: getText(item.Keys[2].Token), + Type: getText(item.Keys[1].Token), + Location: getPos(item.Keys[2].Token, path), // return position of name + } + index.Resources = append(index.Resources, resource) + break + } + + case "output": + { + output := OutputDeclaration{ + Name: getText(item.Keys[1].Token), + Location: getPos(item.Keys[1].Token, path), + } + index.Outputs = append(index.Outputs, output) + break + } + } + } +} + +func literalSubPos(text string, pos token.Pos, start int, path string) token.Pos { + for index, char := range text { + if index == start { + break + } + + if char == '\n' { + pos.Column = 1 + pos.Line++ + } else { + pos.Column++ + } + + pos.Offset++ + } + + pos.Filename = path + return pos +} + +func (index *Index) handleLiteral(literal *ast.LiteralType, path string) { + re := regexp.MustCompile(`\${(var\..*?)}`) + + matches := re.FindAllStringIndex(literal.Token.Text, -1) + if matches == nil { + return + } + + for _, match := range matches { + start := match[0] + 2 // ${ + end := match[1] - 1 // } + name := literal.Token.Text[start+4 : end] + + pos := literalSubPos(literal.Token.Text, literal.Pos(), start, path) + + _, ok := index.References[name] + if !ok { + list := ReferenceList{ + Name: name, + Locations: []token.Pos{pos}, + } + index.References[name] = list + } else { + list := index.References[name] + list.Locations = append(list.Locations, pos) + index.References[name] = list + } + } +} diff --git a/terraform-ast.go b/terraform-ast.go deleted file mode 100644 index 12c3856..0000000 --- a/terraform-ast.go +++ /dev/null @@ -1,147 +0,0 @@ -package main - -import ( - "encoding/json" - "flag" - "io/ioutil" - "os" - "strings" - - "fmt" - - "github.com/hashicorp/hcl" - "github.com/hashicorp/hcl/hcl/ast" - "github.com/hashicorp/hcl/hcl/token" -) - -type VariableDeclaration struct { - Name string - Location token.Pos -} - -type ResourceDeclaration struct { - Type string - Name string - Location token.Pos -} - -type OutputDeclaration struct { - Name string - Location token.Pos -} - -type AstDump struct { - Variables []VariableDeclaration - Resources []ResourceDeclaration - Outputs []OutputDeclaration - RawAst *ast.File -} - -func GetText(t token.Token) string { - return strings.Trim(t.Text, "\"") -} - -func GetPos(t token.Token, path string) token.Pos { - location := t.Pos - location.Filename = path - return location -} - -func CollectDump(astFile *ast.File, path string) (*AstDump, error) { - dump := new(AstDump) - dump.Variables = []VariableDeclaration{} - dump.Resources = []ResourceDeclaration{} - dump.Outputs = []OutputDeclaration{} - dump.RawAst = astFile - - objectList, ok := astFile.Node.(*ast.ObjectList) - if !ok { - return nil, fmt.Errorf("Root node is not an objectList %v", astFile.Node) - } - - for _, item := range objectList.Items { - firstToken := item.Keys[0].Token - if firstToken.Type != 4 { - continue - } - - switch firstToken.Text { - case "variable": - { - variable := VariableDeclaration{ - Name: GetText(item.Keys[1].Token), - Location: GetPos(item.Keys[1].Token, path), - } - dump.Variables = append(dump.Variables, variable) - break - } - - case "resource": - { - resource := ResourceDeclaration{ - Name: GetText(item.Keys[2].Token), - Type: GetText(item.Keys[1].Token), - Location: GetPos(item.Keys[2].Token, path), // return position of name - } - dump.Resources = append(dump.Resources, resource) - break - } - - case "output": - { - output := OutputDeclaration{ - Name: GetText(item.Keys[1].Token), - Location: GetPos(item.Keys[1].Token, path), - } - dump.Outputs = append(dump.Outputs, output) - break - } - } - } - - return dump, nil -} - -func Contents(path string) ([]byte, error) { - if path == "-" { - return ioutil.ReadAll(os.Stdin) - } - - return ioutil.ReadFile(path) -} - -func main() { - includeRaw := flag.Bool("raw-ast", false, "include the raw ast") - path := flag.String("file", "-", "file to parse") - - flag.Parse() - - source, err := Contents(*path) - if err != nil { - fmt.Fprintf(os.Stderr, "ERROR: Cannot open path '%s'\n", *path) - os.Exit(1) - } - - astFile, err := hcl.ParseBytes(source) - if err != nil { - fmt.Fprintf(os.Stderr, "ERROR: Cannot parse '%s'\n", err) - os.Exit(2) - } - - dump, err := CollectDump(astFile, *path) - if err != nil { - fmt.Fprintf(os.Stderr, "ERROR: Could not collect dump '%s'\n", err) - os.Exit(3) - } - - if !*includeRaw { - dump.RawAst = nil - } - - json, err := json.MarshalIndent(dump, "", " ") - if err != nil { - os.Exit(3) - } - - os.Stdout.Write(json) -} diff --git a/terraform-index.go b/terraform-index.go new file mode 100644 index 0000000..b26d8cb --- /dev/null +++ b/terraform-index.go @@ -0,0 +1,61 @@ +package main + +import ( + "encoding/json" + "flag" + "io/ioutil" + "os" + + "fmt" + + "github.com/mauve/terraform-index/index" +) + +const ( + BINARY = "terraform-index" +) + +func Contents(path string) ([]byte, error) { + if path == "-" { + return ioutil.ReadAll(os.Stdin) + } + + return ioutil.ReadFile(path) +} + +func main() { + includeRaw := flag.Bool("raw-ast", false, "include the raw ast") + + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "usage: %s [options] \n\n", BINARY) + fmt.Fprintf(os.Stderr, "Extracts references and declarations from Terraform files\n") + flag.PrintDefaults() + } + flag.Parse() + + if len(flag.Args()) == 0 { + flag.Usage() + os.Exit(1) + } + + index := index.NewIndex() + for _, path := range flag.Args() { + source, err := Contents(path) + if err != nil { + fmt.Fprintf(os.Stderr, "ERROR: Cannot open path '%s': %s\n", path, err) + os.Exit(2) + } + + err = index.CollectString(source, path, *includeRaw) + if err != nil { + fmt.Fprintf(os.Stderr, "ERROR: Could not parse '%s': %s\n", path, err) + } + } + + json, err := json.MarshalIndent(index, "", " ") + if err != nil { + os.Exit(3) + } + + os.Stdout.Write(json) +}