github.com/Azure/tflint-ruleset-azurerm-ext@v0.6.0/rules/resource_block.go (about)

     1  package rules
     2  
     3  import (
     4  	"fmt"
     5  	"github.com/hashicorp/go-multierror"
     6  	"github.com/hashicorp/hcl/v2"
     7  	"github.com/hashicorp/hcl/v2/hclsyntax"
     8  	"github.com/hashicorp/hcl/v2/hclwrite"
     9  	tfjson "github.com/hashicorp/terraform-json"
    10  	"sort"
    11  	"strings"
    12  )
    13  
    14  // Block is an interface offering general APIs on resource/nested block
    15  type Block interface {
    16  	// CheckBlock checks the resourceBlock/nestedBlock recursively to find the block not in order,
    17  	// and invoke the emit function on that block
    18  	CheckBlock() error
    19  
    20  	// ToString prints the sorted block
    21  	ToString() string
    22  
    23  	// DefRange gets the definition range of the block
    24  	DefRange() hcl.Range
    25  }
    26  
    27  // ResourceBlock is the wrapper of a resource block
    28  type ResourceBlock struct {
    29  	File                 *hcl.File
    30  	Block                *hclsyntax.Block
    31  	HeadMetaArgs         *HeadMetaArgs
    32  	RequiredArgs         *Args
    33  	OptionalArgs         *Args
    34  	RequiredNestedBlocks *NestedBlocks
    35  	OptionalNestedBlocks *NestedBlocks
    36  	TailMetaArgs         *Args
    37  	TailMetaNestedBlocks *NestedBlocks
    38  	ParentBlockNames     []string
    39  	emit                 func(block Block) error
    40  }
    41  
    42  // CheckBlock checks the resource block and nested block recursively to find the block not in order,
    43  // and invoke the emit function on that block
    44  func (b *ResourceBlock) CheckBlock() error {
    45  	if !b.CheckOrder() {
    46  		return b.emit(b)
    47  	}
    48  	var err error
    49  	for _, nb := range b.nestedBlocks() {
    50  		if subErr := nb.CheckBlock(); subErr != nil {
    51  			err = multierror.Append(err, subErr)
    52  		}
    53  	}
    54  	return err
    55  }
    56  
    57  // DefRange gets the definition range of the resource block
    58  func (b *ResourceBlock) DefRange() hcl.Range {
    59  	return b.Block.DefRange()
    60  }
    61  
    62  // BuildResourceBlock Build the root block wrapper using hclsyntax.Block
    63  func BuildResourceBlock(block *hclsyntax.Block, file *hcl.File,
    64  	emitter func(block Block) error) *ResourceBlock {
    65  	b := &ResourceBlock{
    66  		File:             file,
    67  		Block:            block,
    68  		ParentBlockNames: []string{block.Type, block.Labels[0]},
    69  		emit:             emitter,
    70  	}
    71  	b.buildArgs(block.Body.Attributes)
    72  	b.buildNestedBlocks(block.Body.Blocks)
    73  	return b
    74  }
    75  
    76  // CheckOrder checks whether the resourceBlock is sorted
    77  func (b *ResourceBlock) CheckOrder() bool {
    78  	return b.sorted() && b.gaped()
    79  }
    80  
    81  // ToString prints the sorted resource block
    82  func (b *ResourceBlock) ToString() string {
    83  	headMetaTxt := toString(b.HeadMetaArgs)
    84  	argTxt := toString(b.RequiredArgs, b.OptionalArgs)
    85  	nbTxt := toString(b.RequiredNestedBlocks, b.OptionalNestedBlocks)
    86  	tailMetaArgTxt := toString(b.TailMetaArgs)
    87  	tailMetaNbTxt := toString(b.TailMetaNestedBlocks)
    88  	var txts []string
    89  	for _, subTxt := range []string{
    90  		headMetaTxt,
    91  		argTxt,
    92  		nbTxt,
    93  		tailMetaArgTxt,
    94  		tailMetaNbTxt} {
    95  		if subTxt != "" {
    96  			txts = append(txts, subTxt)
    97  		}
    98  	}
    99  	txt := strings.Join(txts, "\n\n")
   100  	blockHead := string(b.Block.DefRange().SliceBytes(b.File.Bytes))
   101  	if strings.TrimSpace(txt) == "" {
   102  		txt = fmt.Sprintf("%s {}", blockHead)
   103  	} else {
   104  		txt = fmt.Sprintf("%s {\n%s\n}", blockHead, txt)
   105  	}
   106  	return string(hclwrite.Format([]byte(txt)))
   107  }
   108  
   109  func (b *ResourceBlock) nestedBlocks() []*NestedBlock {
   110  	var nbs []*NestedBlock
   111  	for _, nb := range []*NestedBlocks{
   112  		b.RequiredNestedBlocks,
   113  		b.OptionalNestedBlocks,
   114  		b.TailMetaNestedBlocks} {
   115  		if nb != nil {
   116  			nbs = append(nbs, nb.Blocks...)
   117  		}
   118  	}
   119  	return nbs
   120  }
   121  
   122  func (b *ResourceBlock) buildArgs(attributes hclsyntax.Attributes) {
   123  	resourceBlock := queryBlockSchema(b.ParentBlockNames)
   124  	for _, attr := range attributesByLines(attributes) {
   125  		attrName := attr.Name
   126  		arg := buildAttrArg(attr, b.File)
   127  		if IsHeadMeta(attrName) {
   128  			b.addHeadMetaArg(arg)
   129  			continue
   130  		}
   131  		if IsTailMeta(attrName) {
   132  			b.addTailMetaArg(arg)
   133  			continue
   134  		}
   135  		if resourceBlock == nil {
   136  			b.addOptionalAttr(arg)
   137  			continue
   138  		}
   139  		attrSchema, isAzAttr := resourceBlock.Attributes[attrName]
   140  		if isAzAttr && attrSchema.Required {
   141  			b.addRequiredAttr(arg)
   142  		} else {
   143  			b.addOptionalAttr(arg)
   144  		}
   145  	}
   146  }
   147  
   148  func attributesByLines(attributes hclsyntax.Attributes) []*hclsyntax.Attribute {
   149  	var attrs []*hclsyntax.Attribute
   150  	for _, attr := range attributes {
   151  		attrs = append(attrs, attr)
   152  	}
   153  	sort.Slice(attrs, func(i, j int) bool {
   154  		return attrs[i].Range().Start.Line < attrs[j].Range().Start.Line
   155  	})
   156  	return attrs
   157  }
   158  
   159  func (b *ResourceBlock) buildNestedBlock(nestedBlock *hclsyntax.Block) *NestedBlock {
   160  	nestedBlockName := nestedBlock.Type
   161  	sortField := nestedBlock.Type
   162  	if nestedBlock.Type == "dynamic" {
   163  		nestedBlockName = nestedBlock.Labels[0]
   164  		sortField = strings.Join(nestedBlock.Labels, "")
   165  	}
   166  	parentBlockNames := append(b.ParentBlockNames, nestedBlockName)
   167  	if b.Block.Type == "dynamic" && nestedBlockName == "content" {
   168  		parentBlockNames = b.ParentBlockNames
   169  	}
   170  	nb := &NestedBlock{
   171  		Name:             nestedBlockName,
   172  		SortField:        sortField,
   173  		Range:            nestedBlock.Range(),
   174  		Block:            nestedBlock,
   175  		ParentBlockNames: parentBlockNames,
   176  		File:             b.File,
   177  		emit:             b.emit,
   178  	}
   179  	nb.buildAttributes(nestedBlock.Body.Attributes)
   180  	nb.buildNestedBlocks(nestedBlock.Body.Blocks)
   181  	return nb
   182  }
   183  
   184  func (b *ResourceBlock) buildNestedBlocks(nestedBlocks hclsyntax.Blocks) {
   185  	blockSchema := queryBlockSchema(b.ParentBlockNames)
   186  	for _, nestedBlock := range nestedBlocks {
   187  		nb := b.buildNestedBlock(nestedBlock)
   188  		if IsTailMeta(nb.Name) {
   189  			b.addTailMetaNestedBlock(nb)
   190  			continue
   191  		}
   192  		if metaArgOrUnknownBlock(blockSchema) {
   193  			b.addOptionalNestedBlock(nb)
   194  			continue
   195  		}
   196  		blockSchema, isAzNestedBlock := blockSchema.NestedBlocks[nb.Name]
   197  		if isAzNestedBlock && blockSchema.MinItems > 0 {
   198  			b.addRequiredNestedBlock(nb)
   199  		} else {
   200  			b.addOptionalNestedBlock(nb)
   201  		}
   202  	}
   203  }
   204  
   205  func metaArgOrUnknownBlock(blockSchema *tfjson.SchemaBlock) bool {
   206  	return blockSchema == nil || blockSchema.NestedBlocks == nil
   207  }
   208  
   209  func (b *ResourceBlock) sorted() bool {
   210  	sections := []Section{
   211  		b.HeadMetaArgs,
   212  		b.RequiredArgs,
   213  		b.OptionalArgs,
   214  		b.RequiredNestedBlocks,
   215  		b.OptionalNestedBlocks,
   216  		b.TailMetaArgs,
   217  		b.TailMetaNestedBlocks,
   218  	}
   219  	lastEndLine := -1
   220  	for _, s := range sections {
   221  		if !s.CheckOrder() {
   222  			return false
   223  		}
   224  		r := s.GetRange()
   225  		if r == nil {
   226  			continue
   227  		}
   228  		if r.Start.Line <= lastEndLine {
   229  			return false
   230  		}
   231  		lastEndLine = r.End.Line
   232  	}
   233  	return true
   234  }
   235  
   236  func (b *ResourceBlock) gaped() bool {
   237  	ranges := []*hcl.Range{
   238  		b.HeadMetaArgs.GetRange(),
   239  		mergeRange(b.RequiredArgs, b.OptionalArgs),
   240  		mergeRange(b.RequiredNestedBlocks, b.OptionalNestedBlocks),
   241  		b.TailMetaArgs.GetRange(),
   242  		b.TailMetaNestedBlocks.GetRange(),
   243  	}
   244  	lastEndLine := -2
   245  	for _, r := range ranges {
   246  		if r == nil {
   247  			continue
   248  		}
   249  		if r.Start.Line-lastEndLine < 2 {
   250  			return false
   251  		}
   252  		lastEndLine = r.End.Line
   253  	}
   254  	return true
   255  }
   256  
   257  func (b *ResourceBlock) addHeadMetaArg(arg *Arg) {
   258  	if b.HeadMetaArgs == nil {
   259  		b.HeadMetaArgs = &HeadMetaArgs{}
   260  	}
   261  	b.HeadMetaArgs.add(arg)
   262  }
   263  
   264  func (b *ResourceBlock) addTailMetaArg(arg *Arg) {
   265  	if b.TailMetaArgs == nil {
   266  		b.TailMetaArgs = &Args{}
   267  	}
   268  	b.TailMetaArgs.add(arg)
   269  }
   270  
   271  func (b *ResourceBlock) addRequiredAttr(arg *Arg) {
   272  	if b.RequiredArgs == nil {
   273  		b.RequiredArgs = &Args{}
   274  	}
   275  	b.RequiredArgs.add(arg)
   276  }
   277  
   278  func (b *ResourceBlock) addOptionalAttr(arg *Arg) {
   279  	if b.OptionalArgs == nil {
   280  		b.OptionalArgs = &Args{}
   281  	}
   282  	b.OptionalArgs.add(arg)
   283  }
   284  
   285  func (b *ResourceBlock) addTailMetaNestedBlock(nb *NestedBlock) {
   286  	if b.TailMetaNestedBlocks == nil {
   287  		b.TailMetaNestedBlocks = &NestedBlocks{}
   288  	}
   289  	b.TailMetaNestedBlocks.add(nb)
   290  }
   291  
   292  func (b *ResourceBlock) addRequiredNestedBlock(nb *NestedBlock) {
   293  	if b.RequiredNestedBlocks == nil {
   294  		b.RequiredNestedBlocks = &NestedBlocks{}
   295  	}
   296  	b.RequiredNestedBlocks.add(nb)
   297  }
   298  
   299  func (b *ResourceBlock) addOptionalNestedBlock(nb *NestedBlock) {
   300  	if b.OptionalNestedBlocks == nil {
   301  		b.OptionalNestedBlocks = &NestedBlocks{}
   302  	}
   303  	b.OptionalNestedBlocks.add(nb)
   304  }