github.com/jpreese/tflint@v0.19.2-0.20200908152133-b01686250fb6/rules/awsrules/models/generator/main.go (about) 1 // +build generators 2 3 package main 4 5 import ( 6 "encoding/json" 7 "fmt" 8 "io/ioutil" 9 "path/filepath" 10 "sort" 11 12 hcl "github.com/hashicorp/hcl/v2" 13 "github.com/hashicorp/hcl/v2/gohcl" 14 "github.com/hashicorp/hcl/v2/hclparse" 15 "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 16 "github.com/terraform-providers/terraform-provider-aws/aws" 17 ) 18 19 type mappingFile struct { 20 Import string `hcl:"import"` 21 Mappings []mapping `hcl:"mapping,block"` 22 Tests []test `hcl:"test,block"` 23 } 24 25 type mapping struct { 26 Resource string `hcl:"resource,label"` 27 Attrs hcl.Attributes `hcl:",remain"` 28 } 29 30 type test struct { 31 Resource string `hcl:"resource,label"` 32 Attribute string `hcl:"attribute,label"` 33 OK string `hcl:"ok"` 34 NG string `hcl:"ng"` 35 } 36 37 func main() { 38 files, err := filepath.Glob("./mappings/*.hcl") 39 if err != nil { 40 panic(err) 41 } 42 43 mappingFiles := []mappingFile{} 44 for _, file := range files { 45 parser := hclparse.NewParser() 46 f, diags := parser.ParseHCLFile(file) 47 if diags.HasErrors() { 48 panic(diags) 49 } 50 51 var mf mappingFile 52 diags = gohcl.DecodeBody(f.Body, nil, &mf) 53 if diags.HasErrors() { 54 panic(diags) 55 } 56 mappingFiles = append(mappingFiles, mf) 57 } 58 59 awsProvider := aws.Provider() 60 61 generatedRules := []string{} 62 for _, mappingFile := range mappingFiles { 63 raw, err := ioutil.ReadFile(mappingFile.Import) 64 if err != nil { 65 panic(err) 66 } 67 68 var api map[string]interface{} 69 err = json.Unmarshal(raw, &api) 70 if err != nil { 71 panic(err) 72 } 73 shapes := api["shapes"].(map[string]interface{}) 74 75 for _, mapping := range mappingFile.Mappings { 76 for attribute, value := range mapping.Attrs { 77 shapeName := value.Expr.Variables()[0].RootName() 78 if shapeName == "any" { 79 continue 80 } 81 model := shapes[shapeName].(map[string]interface{}) 82 schema := fetchSchema(mapping.Resource, attribute, model, awsProvider) 83 if validMapping(model) { 84 fmt.Printf("Generating rule for `%s.%s`\n", mapping.Resource, attribute) 85 generateRuleFile(mapping.Resource, attribute, model, schema) 86 for _, test := range mappingFile.Tests { 87 if mapping.Resource == test.Resource && attribute == test.Attribute { 88 generateRuleTestFile(mapping.Resource, attribute, model, test) 89 } 90 } 91 generatedRules = append(generatedRules, makeRuleName(mapping.Resource, attribute)) 92 } 93 } 94 } 95 } 96 97 sort.Strings(generatedRules) 98 generateProviderFile(generatedRules) 99 } 100 101 func fetchSchema(resource, attribute string, model map[string]interface{}, provider *schema.Provider) *schema.Schema { 102 resourceSchema, ok := provider.ResourcesMap[resource] 103 if !ok { 104 panic(fmt.Sprintf("resource `%s` not found in the Terraform schema", resource)) 105 } 106 attrSchema, ok := resourceSchema.Schema[attribute] 107 if !ok { 108 panic(fmt.Sprintf("`%s.%s` not found in the Terraform schema", resource, attribute)) 109 } 110 111 switch model["type"].(string) { 112 case "string": 113 if attrSchema.Type != schema.TypeString { 114 panic(fmt.Sprintf("`%s.%s` is expected as string, but not", resource, attribute)) 115 } 116 default: 117 // noop 118 } 119 120 return attrSchema 121 } 122 123 func validMapping(model map[string]interface{}) bool { 124 switch model["type"].(string) { 125 case "string": 126 if _, ok := model["max"]; ok { 127 return true 128 } 129 if min, ok := model["min"]; ok && int(min.(float64)) > 2 { 130 return true 131 } 132 if _, ok := model["pattern"]; ok { 133 return true 134 } 135 if _, ok := model["enum"]; ok { 136 return true 137 } 138 return false 139 default: 140 // Unsupported types 141 return false 142 } 143 }