213 строки
4.6 KiB
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")
|
|
}
|