rules/resource_block.go (267 lines of code) (raw):
package rules
import (
"fmt"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/hclwrite"
tfjson "github.com/hashicorp/terraform-json"
"sort"
"strings"
)
// Block is an interface offering general APIs on resource/nested block
type Block interface {
// CheckBlock checks the resourceBlock/nestedBlock recursively to find the block not in order,
// and invoke the emit function on that block
CheckBlock() error
// ToString prints the sorted block
ToString() string
// DefRange gets the definition range of the block
DefRange() hcl.Range
}
// ResourceBlock is the wrapper of a resource block
type ResourceBlock struct {
File *hcl.File
Block *hclsyntax.Block
HeadMetaArgs *HeadMetaArgs
RequiredArgs *Args
OptionalArgs *Args
RequiredNestedBlocks *NestedBlocks
OptionalNestedBlocks *NestedBlocks
TailMetaArgs *Args
TailMetaNestedBlocks *NestedBlocks
ParentBlockNames []string
emit func(block Block) error
}
// CheckBlock checks the resource block and nested block recursively to find the block not in order,
// and invoke the emit function on that block
func (b *ResourceBlock) CheckBlock() error {
if !b.CheckOrder() {
return b.emit(b)
}
var err error
for _, nb := range b.nestedBlocks() {
if subErr := nb.CheckBlock(); subErr != nil {
err = multierror.Append(err, subErr)
}
}
return err
}
// DefRange gets the definition range of the resource block
func (b *ResourceBlock) DefRange() hcl.Range {
return b.Block.DefRange()
}
// BuildResourceBlock Build the root block wrapper using hclsyntax.Block
func BuildResourceBlock(block *hclsyntax.Block, file *hcl.File,
emitter func(block Block) error) *ResourceBlock {
b := &ResourceBlock{
File: file,
Block: block,
ParentBlockNames: []string{block.Type, block.Labels[0]},
emit: emitter,
}
b.buildArgs(block.Body.Attributes)
b.buildNestedBlocks(block.Body.Blocks)
return b
}
// CheckOrder checks whether the resourceBlock is sorted
func (b *ResourceBlock) CheckOrder() bool {
return b.sorted() && b.gaped()
}
// ToString prints the sorted resource block
func (b *ResourceBlock) ToString() string {
headMetaTxt := toString(b.HeadMetaArgs)
argTxt := toString(b.RequiredArgs, b.OptionalArgs)
nbTxt := toString(b.RequiredNestedBlocks, b.OptionalNestedBlocks)
tailMetaArgTxt := toString(b.TailMetaArgs)
tailMetaNbTxt := toString(b.TailMetaNestedBlocks)
var txts []string
for _, subTxt := range []string{
headMetaTxt,
argTxt,
nbTxt,
tailMetaArgTxt,
tailMetaNbTxt} {
if subTxt != "" {
txts = append(txts, subTxt)
}
}
txt := strings.Join(txts, "\n\n")
blockHead := string(b.Block.DefRange().SliceBytes(b.File.Bytes))
if strings.TrimSpace(txt) == "" {
txt = fmt.Sprintf("%s {}", blockHead)
} else {
txt = fmt.Sprintf("%s {\n%s\n}", blockHead, txt)
}
return string(hclwrite.Format([]byte(txt)))
}
func (b *ResourceBlock) nestedBlocks() []*NestedBlock {
var nbs []*NestedBlock
for _, nb := range []*NestedBlocks{
b.RequiredNestedBlocks,
b.OptionalNestedBlocks,
b.TailMetaNestedBlocks} {
if nb != nil {
nbs = append(nbs, nb.Blocks...)
}
}
return nbs
}
func (b *ResourceBlock) buildArgs(attributes hclsyntax.Attributes) {
resourceBlock := queryBlockSchema(b.ParentBlockNames)
for _, attr := range attributesByLines(attributes) {
attrName := attr.Name
arg := buildAttrArg(attr, b.File)
if IsHeadMeta(attrName) {
b.addHeadMetaArg(arg)
continue
}
if IsTailMeta(attrName) {
b.addTailMetaArg(arg)
continue
}
if resourceBlock == nil {
b.addOptionalAttr(arg)
continue
}
attrSchema, isAzAttr := resourceBlock.Attributes[attrName]
if isAzAttr && attrSchema.Required {
b.addRequiredAttr(arg)
} else {
b.addOptionalAttr(arg)
}
}
}
func attributesByLines(attributes hclsyntax.Attributes) []*hclsyntax.Attribute {
var attrs []*hclsyntax.Attribute
for _, attr := range attributes {
attrs = append(attrs, attr)
}
sort.Slice(attrs, func(i, j int) bool {
return attrs[i].Range().Start.Line < attrs[j].Range().Start.Line
})
return attrs
}
func (b *ResourceBlock) buildNestedBlock(nestedBlock *hclsyntax.Block) *NestedBlock {
nestedBlockName := nestedBlock.Type
sortField := nestedBlock.Type
if nestedBlock.Type == "dynamic" {
nestedBlockName = nestedBlock.Labels[0]
sortField = strings.Join(nestedBlock.Labels, "")
}
parentBlockNames := append(b.ParentBlockNames, nestedBlockName)
if b.Block.Type == "dynamic" && nestedBlockName == "content" {
parentBlockNames = b.ParentBlockNames
}
nb := &NestedBlock{
Name: nestedBlockName,
SortField: sortField,
Range: nestedBlock.Range(),
Block: nestedBlock,
ParentBlockNames: parentBlockNames,
File: b.File,
emit: b.emit,
}
nb.buildAttributes(nestedBlock.Body.Attributes)
nb.buildNestedBlocks(nestedBlock.Body.Blocks)
return nb
}
func (b *ResourceBlock) buildNestedBlocks(nestedBlocks hclsyntax.Blocks) {
blockSchema := queryBlockSchema(b.ParentBlockNames)
for _, nestedBlock := range nestedBlocks {
nb := b.buildNestedBlock(nestedBlock)
if IsTailMeta(nb.Name) {
b.addTailMetaNestedBlock(nb)
continue
}
if metaArgOrUnknownBlock(blockSchema) {
b.addOptionalNestedBlock(nb)
continue
}
blockSchema, isAzNestedBlock := blockSchema.NestedBlocks[nb.Name]
if isAzNestedBlock && blockSchema.MinItems > 0 {
b.addRequiredNestedBlock(nb)
} else {
b.addOptionalNestedBlock(nb)
}
}
}
func metaArgOrUnknownBlock(blockSchema *tfjson.SchemaBlock) bool {
return blockSchema == nil || blockSchema.NestedBlocks == nil
}
func (b *ResourceBlock) sorted() bool {
sections := []Section{
b.HeadMetaArgs,
b.RequiredArgs,
b.OptionalArgs,
b.RequiredNestedBlocks,
b.OptionalNestedBlocks,
b.TailMetaArgs,
b.TailMetaNestedBlocks,
}
lastEndLine := -1
for _, s := range sections {
if !s.CheckOrder() {
return false
}
r := s.GetRange()
if r == nil {
continue
}
if r.Start.Line <= lastEndLine {
return false
}
lastEndLine = r.End.Line
}
return true
}
func (b *ResourceBlock) gaped() bool {
ranges := []*hcl.Range{
b.HeadMetaArgs.GetRange(),
mergeRange(b.RequiredArgs, b.OptionalArgs),
mergeRange(b.RequiredNestedBlocks, b.OptionalNestedBlocks),
b.TailMetaArgs.GetRange(),
b.TailMetaNestedBlocks.GetRange(),
}
lastEndLine := -2
for _, r := range ranges {
if r == nil {
continue
}
if r.Start.Line-lastEndLine < 2 {
return false
}
lastEndLine = r.End.Line
}
return true
}
func (b *ResourceBlock) addHeadMetaArg(arg *Arg) {
if b.HeadMetaArgs == nil {
b.HeadMetaArgs = &HeadMetaArgs{}
}
b.HeadMetaArgs.add(arg)
}
func (b *ResourceBlock) addTailMetaArg(arg *Arg) {
if b.TailMetaArgs == nil {
b.TailMetaArgs = &Args{}
}
b.TailMetaArgs.add(arg)
}
func (b *ResourceBlock) addRequiredAttr(arg *Arg) {
if b.RequiredArgs == nil {
b.RequiredArgs = &Args{}
}
b.RequiredArgs.add(arg)
}
func (b *ResourceBlock) addOptionalAttr(arg *Arg) {
if b.OptionalArgs == nil {
b.OptionalArgs = &Args{}
}
b.OptionalArgs.add(arg)
}
func (b *ResourceBlock) addTailMetaNestedBlock(nb *NestedBlock) {
if b.TailMetaNestedBlocks == nil {
b.TailMetaNestedBlocks = &NestedBlocks{}
}
b.TailMetaNestedBlocks.add(nb)
}
func (b *ResourceBlock) addRequiredNestedBlock(nb *NestedBlock) {
if b.RequiredNestedBlocks == nil {
b.RequiredNestedBlocks = &NestedBlocks{}
}
b.RequiredNestedBlocks.add(nb)
}
func (b *ResourceBlock) addOptionalNestedBlock(nb *NestedBlock) {
if b.OptionalNestedBlocks == nil {
b.OptionalNestedBlocks = &NestedBlocks{}
}
b.OptionalNestedBlocks.add(nb)
}