terraform-module-test-helper/module.go

213 строки
4.6 KiB
Go

package terraform_module_test_helper
import (
"path/filepath"
"strings"
"github.com/ahmetb/go-linq/v3"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclparse"
"github.com/hashicorp/terraform-config-inspect/tfconfig"
"github.com/spf13/afero"
)
type Module struct {
*tfconfig.Module
OutputExts map[string]Output
VariableExts map[string]Variable
fs afero.Afero
}
type Output struct {
Name string
Description string
Sensitive string
Value string
Range hcl.Range
}
type Variable struct {
Name string
Type string
Description string
Default string
Sensitive string
Nullable string
Range hcl.Range
}
func NewModule(dir string, fs afero.Afero) (*Module, error) {
m, diag := tfconfig.LoadModule(dir)
if diag.HasErrors() {
return nil, diag
}
return &Module{
Module: m,
OutputExts: make(map[string]Output),
VariableExts: make(map[string]Variable),
fs: fs,
}, nil
}
func (m *Module) Load() error {
fileNames := m.codeFileNames()
parser := hclparse.NewParser()
for _, n := range fileNames {
content, err := m.fs.ReadFile(n)
if err != nil {
return err
}
var f *hcl.File
var diag hcl.Diagnostics
if fileExt(n) == ".tf" {
f, diag = parser.ParseHCL(content, n)
} else {
f, diag = parser.ParseJSON(content, n)
}
if diag.HasErrors() {
return diag
}
c, _, diag := f.Body.PartialContent(&hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{
Type: "variable",
LabelNames: []string{"name"},
},
{
Type: "output",
LabelNames: []string{"name"},
},
},
})
if diag.HasErrors() {
return diag
}
for _, b := range c.Blocks {
switch b.Type {
case "variable":
{
v := m.parseVariable(b, f)
m.VariableExts[b.Labels[0]] = v
}
case "output":
{
o := m.parseOutput(b, f)
m.OutputExts[b.Labels[0]] = o
}
}
}
}
return nil
}
func (m *Module) codeFileNames() []string {
var fileNames []string
linq.From(m.Variables).Select(func(i interface{}) interface{} {
return i.(linq.KeyValue).Value.(*tfconfig.Variable).Pos.Filename
}).Distinct().Where(func(i interface{}) bool {
n := filepath.Base(i.(string))
// For now we only support tf, no json.
return fileExt(n) != "" && !isOverride(n) && !isIgnoredFile(n)
}).ToSlice(&fileNames)
return fileNames
}
func (m *Module) parseOutput(b *hcl.Block, f *hcl.File) Output {
content, _, _ := b.Body.PartialContent(&hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: "value",
Required: true,
},
{
Name: "description",
},
{
Name: "sensitive",
},
},
})
attributes := content.Attributes
o := Output{
Name: b.Labels[0],
Range: b.DefRange,
Value: attributeValueString(attributes["value"], f),
}
if desc, ok := attributes["description"]; ok {
o.Description = attributeValueString(desc, f)
}
if sensitive, ok := attributes["sensitive"]; ok {
o.Sensitive = attributeValueString(sensitive, f)
}
// We don't compare position's change
o.Range = hcl.Range{}
return o
}
func (m *Module) parseVariable(b *hcl.Block, f *hcl.File) Variable {
content, _, _ := b.Body.PartialContent(&hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: "description",
},
{
Name: "sensitive",
},
{
Name: "default",
},
{
Name: "nullable",
},
{
Name: "type",
},
},
})
attributes := content.Attributes
v := Variable{
Name: b.Labels[0],
Range: b.DefRange,
}
if desc, ok := attributes["description"]; ok {
v.Description = attributeValueString(desc, f)
}
if sensitive, ok := attributes["sensitive"]; ok {
v.Sensitive = attributeValueString(sensitive, f)
}
if defaultValue, ok := attributes["default"]; ok {
v.Default = attributeValueString(defaultValue, f)
}
if nullable, ok := attributes["nullable"]; ok {
v.Nullable = attributeValueString(nullable, f)
}
if t, ok := attributes["type"]; ok {
v.Type = attributeValueString(t, f)
}
// We don't compare position's change
v.Range = hcl.Range{}
return v
}
func fileExt(path string) string {
if strings.HasSuffix(path, ".tf") {
return ".tf"
} else if strings.HasSuffix(path, ".tf.json") {
return ".tf.json"
} else {
return ""
}
}
func isIgnoredFile(name string) bool {
return strings.HasPrefix(name, ".") || // Unix-like hidden files
strings.HasSuffix(name, "~") || // vim
strings.HasPrefix(name, "#") && strings.HasSuffix(name, "#") // emacs
}
func isOverride(name string) bool {
ext := fileExt(name)
baseName := name[:len(name)-len(ext)] // strip extension
return baseName == "override" || strings.HasSuffix(baseName, "_override")
}