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 }