github.com/terraform-linters/tflint@v0.51.2-0.20240520175844-3750771571b6/terraform/module.go (about)

     1  package terraform
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"github.com/hashicorp/hcl/v2"
     8  	"github.com/hashicorp/hcl/v2/hclsyntax"
     9  	hcljson "github.com/hashicorp/hcl/v2/json"
    10  	"github.com/terraform-linters/tflint-plugin-sdk/hclext"
    11  )
    12  
    13  type Module struct {
    14  	Resources   map[string]map[string]*Resource
    15  	Variables   map[string]*Variable
    16  	Locals      map[string]*Local
    17  	ModuleCalls map[string]*ModuleCall
    18  
    19  	SourceDir string
    20  
    21  	Sources map[string][]byte
    22  	Files   map[string]*hcl.File
    23  
    24  	primaries map[string]*hcl.File
    25  	overrides map[string]*hcl.File
    26  }
    27  
    28  func NewEmptyModule() *Module {
    29  	return &Module{
    30  		Resources:   map[string]map[string]*Resource{},
    31  		Variables:   map[string]*Variable{},
    32  		Locals:      map[string]*Local{},
    33  		ModuleCalls: map[string]*ModuleCall{},
    34  
    35  		SourceDir: "",
    36  
    37  		Sources: map[string][]byte{},
    38  		Files:   map[string]*hcl.File{},
    39  
    40  		primaries: map[string]*hcl.File{},
    41  		overrides: map[string]*hcl.File{},
    42  	}
    43  }
    44  
    45  func (m *Module) build() hcl.Diagnostics {
    46  	body, diags := m.PartialContent(moduleSchema, nil)
    47  	if diags.HasErrors() {
    48  		return diags
    49  	}
    50  
    51  	for _, block := range body.Blocks {
    52  		switch block.Type {
    53  		case "resource":
    54  			r := decodeResourceBlock(block)
    55  			if _, exists := m.Resources[r.Type]; !exists {
    56  				m.Resources[r.Type] = map[string]*Resource{}
    57  			}
    58  			m.Resources[r.Type][r.Name] = r
    59  		case "variable":
    60  			v, valDiags := decodeVairableBlock(block)
    61  			diags = diags.Extend(valDiags)
    62  			m.Variables[v.Name] = v
    63  		case "module":
    64  			call, moduleDiags := decodeModuleBlock(block)
    65  			diags = diags.Extend(moduleDiags)
    66  			m.ModuleCalls[call.Name] = call
    67  		case "locals":
    68  			locals := decodeLocalsBlock(block)
    69  			for _, local := range locals {
    70  				m.Locals[local.Name] = local
    71  			}
    72  		}
    73  	}
    74  
    75  	return diags
    76  }
    77  
    78  // Rebuild rebuilds the module from the passed sources.
    79  // The main purpose of this is to apply autofixes in the module.
    80  func (m *Module) Rebuild(sources map[string][]byte) hcl.Diagnostics {
    81  	if len(sources) == 0 {
    82  		return nil
    83  	}
    84  	var diags hcl.Diagnostics
    85  
    86  	for path, source := range sources {
    87  		var file *hcl.File
    88  		var d hcl.Diagnostics
    89  		if strings.HasSuffix(path, ".json") {
    90  			file, d = hcljson.Parse(source, path)
    91  		} else {
    92  			file, d = hclsyntax.ParseConfig(source, path, hcl.InitialPos)
    93  		}
    94  		if d.HasErrors() {
    95  			diags = diags.Extend(d)
    96  			continue
    97  		}
    98  
    99  		m.Sources[path] = source
   100  		m.Files[path] = file
   101  		if _, exists := m.primaries[path]; exists {
   102  			m.primaries[path] = file
   103  		}
   104  		if _, exists := m.overrides[path]; exists {
   105  			m.overrides[path] = file
   106  		}
   107  	}
   108  
   109  	d := m.build()
   110  	diags = diags.Extend(d)
   111  	return diags
   112  }
   113  
   114  // PartialContent extracts body content from Terraform configurations based on the passed schema.
   115  // Basically, this function is a wrapper for hclext.PartialContent, but in some ways it reproduces
   116  // Terraform language semantics.
   117  //
   118  //  1. Supports overriding files
   119  //     https://developer.hashicorp.com/terraform/language/files/override
   120  //  2. Expands "dynamic" blocks
   121  //     https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks
   122  //  3. Expands resource/module depends on the meta-arguments
   123  //     https://developer.hashicorp.com/terraform/language/meta-arguments/count
   124  //     https://developer.hashicorp.com/terraform/language/meta-arguments/for_each
   125  //
   126  // But 2 and 3 won't run if you didn't pass the evaluation context.
   127  func (m *Module) PartialContent(schema *hclext.BodySchema, ctx *Evaluator) (*hclext.BodyContent, hcl.Diagnostics) {
   128  	content := &hclext.BodyContent{}
   129  	diags := hcl.Diagnostics{}
   130  
   131  	for _, f := range m.primaries {
   132  		expanded, d := ctx.ExpandBlock(f.Body, schema)
   133  		diags = diags.Extend(d)
   134  		c, d := hclext.PartialContent(expanded, schema)
   135  		diags = diags.Extend(d)
   136  		for name, attr := range c.Attributes {
   137  			content.Attributes[name] = attr
   138  		}
   139  		content.Blocks = append(content.Blocks, c.Blocks...)
   140  	}
   141  	for _, f := range m.overrides {
   142  		expanded, d := ctx.ExpandBlock(f.Body, schema)
   143  		diags = diags.Extend(d)
   144  		c, d := hclext.PartialContent(expanded, schema)
   145  		diags = diags.Extend(d)
   146  		for name, attr := range c.Attributes {
   147  			content.Attributes[name] = attr
   148  		}
   149  		content.Blocks = overrideBlocks(content.Blocks, c.Blocks)
   150  	}
   151  
   152  	return content, diags
   153  }
   154  
   155  // overrideBlocks changes the attributes in the passed primary blocks by override blocks recursively.
   156  func overrideBlocks(primaries, overrides hclext.Blocks) hclext.Blocks {
   157  	dict := map[string]*hclext.Block{}
   158  	for _, primary := range primaries {
   159  		key := fmt.Sprintf("%s[%s]", primary.Type, strings.Join(primary.Labels, ","))
   160  		dict[key] = primary
   161  	}
   162  
   163  	for _, override := range overrides {
   164  		key := fmt.Sprintf("%s[%s]", override.Type, strings.Join(override.Labels, ","))
   165  		if primary, exists := dict[key]; exists {
   166  			for name, attr := range override.Body.Attributes {
   167  				primary.Body.Attributes[name] = attr
   168  			}
   169  			primary.Body.Blocks = overrideBlocks(primary.Body.Blocks, override.Body.Blocks)
   170  		}
   171  	}
   172  
   173  	return primaries
   174  }
   175  
   176  var moduleSchema = &hclext.BodySchema{
   177  	Blocks: []hclext.BlockSchema{
   178  		{
   179  			Type:       "resource",
   180  			LabelNames: []string{"type", "name"},
   181  		},
   182  		{
   183  			Type:       "variable",
   184  			LabelNames: []string{"name"},
   185  			Body:       variableBlockSchema,
   186  		},
   187  		{
   188  			Type:       "module",
   189  			LabelNames: []string{"name"},
   190  			Body:       moduleBlockSchema,
   191  		},
   192  		{
   193  			Type: "locals",
   194  			Body: localBlockSchema,
   195  		},
   196  	},
   197  }