github.com/vtorhonen/terraform@v0.9.0-beta2.0.20170307220345-5d894e4ffda7/scripts/generate-plugins.go (about) 1 // Generate Plugins is a small program that updates the lists of plugins in 2 // command/internal_plugin_list.go so they will be compiled into the main 3 // terraform binary. 4 package main 5 6 import ( 7 "fmt" 8 "go/ast" 9 "go/parser" 10 "go/token" 11 "io/ioutil" 12 "log" 13 "os" 14 "path/filepath" 15 "sort" 16 "strings" 17 ) 18 19 const target = "command/internal_plugin_list.go" 20 21 func main() { 22 wd, _ := os.Getwd() 23 if filepath.Base(wd) != "terraform" { 24 log.Fatalf("This program must be invoked in the terraform project root; in %s", wd) 25 } 26 27 // Collect all of the data we need about plugins we have in the project 28 providers, err := discoverProviders() 29 if err != nil { 30 log.Fatalf("Failed to discover providers: %s", err) 31 } 32 33 provisioners, err := discoverProvisioners() 34 if err != nil { 35 log.Fatalf("Failed to discover provisioners: %s", err) 36 } 37 38 // Do some simple code generation and templating 39 output := source 40 output = strings.Replace(output, "IMPORTS", makeImports(providers, provisioners), 1) 41 output = strings.Replace(output, "PROVIDERS", makeProviderMap(providers), 1) 42 output = strings.Replace(output, "PROVISIONERS", makeProvisionerMap(provisioners), 1) 43 44 // TODO sort the lists of plugins so we are not subjected to random OS ordering of the plugin lists 45 46 // Write our generated code to the command/plugin.go file 47 file, err := os.Create(target) 48 defer file.Close() 49 if err != nil { 50 log.Fatalf("Failed to open %s for writing: %s", target, err) 51 } 52 53 _, err = file.WriteString(output) 54 if err != nil { 55 log.Fatalf("Failed writing to %s: %s", target, err) 56 } 57 58 log.Printf("Generated %s", target) 59 } 60 61 type plugin struct { 62 Package string // Package name from ast remoteexec 63 PluginName string // Path via deriveName() remote-exec 64 TypeName string // Type of plugin provisioner 65 Path string // Relative import path builtin/provisioners/remote-exec 66 ImportName string // See deriveImport() remoteexecprovisioner 67 } 68 69 // makeProviderMap creates a map of providers like this: 70 // 71 // var InternalProviders = map[string]plugin.ProviderFunc{ 72 // "aws": aws.Provider, 73 // "azurerm": azurerm.Provider, 74 // "cloudflare": cloudflare.Provider, 75 func makeProviderMap(items []plugin) string { 76 output := "" 77 for _, item := range items { 78 output += fmt.Sprintf("\t\"%s\": %s.%s,\n", item.PluginName, item.ImportName, item.TypeName) 79 } 80 return output 81 } 82 83 // makeProvisionerMap creates a map of provisioners like this: 84 // 85 // "file": func() terraform.ResourceProvisioner { return new(file.ResourceProvisioner) }, 86 // "local-exec": func() terraform.ResourceProvisioner { return new(localexec.ResourceProvisioner) }, 87 // "remote-exec": func() terraform.ResourceProvisioner { return new(remoteexec.ResourceProvisioner) }, 88 // 89 // This is more verbose than the Provider case because there is no corresponding 90 // Provisioner function. 91 func makeProvisionerMap(items []plugin) string { 92 output := "" 93 for _, item := range items { 94 output += fmt.Sprintf("\t\"%s\": %s.%s,\n", item.PluginName, item.ImportName, item.TypeName) 95 } 96 return output 97 } 98 99 func makeImports(providers, provisioners []plugin) string { 100 plugins := []string{} 101 102 for _, provider := range providers { 103 plugins = append(plugins, fmt.Sprintf("\t%s \"github.com/hashicorp/terraform/%s\"\n", provider.ImportName, filepath.ToSlash(provider.Path))) 104 } 105 106 for _, provisioner := range provisioners { 107 plugins = append(plugins, fmt.Sprintf("\t%s \"github.com/hashicorp/terraform/%s\"\n", provisioner.ImportName, filepath.ToSlash(provisioner.Path))) 108 } 109 110 // Make things pretty 111 sort.Strings(plugins) 112 113 return strings.Join(plugins, "") 114 } 115 116 // listDirectories recursively lists directories under the specified path 117 func listDirectories(path string) ([]string, error) { 118 names := []string{} 119 items, err := ioutil.ReadDir(path) 120 if err != nil { 121 return names, err 122 } 123 124 for _, item := range items { 125 // We only want directories 126 if item.IsDir() { 127 if item.Name() == "test-fixtures" { 128 continue 129 } 130 currentDir := filepath.Join(path, item.Name()) 131 names = append(names, currentDir) 132 133 // Do some recursion 134 subNames, err := listDirectories(currentDir) 135 if err == nil { 136 names = append(names, subNames...) 137 } 138 } 139 } 140 141 return names, nil 142 } 143 144 // deriveName determines the name of the plugin relative to the specified root 145 // path. 146 func deriveName(root, full string) string { 147 short, _ := filepath.Rel(root, full) 148 bits := strings.Split(short, string(os.PathSeparator)) 149 return strings.Join(bits, "-") 150 } 151 152 // deriveImport will build a unique import identifier based on packageName and 153 // the result of deriveName(). This is important for disambigutating between 154 // providers and provisioners that have the same name. This will be something 155 // like: 156 // 157 // remote-exec -> remoteexecprovisioner 158 // 159 // which is long, but is deterministic and unique. 160 func deriveImport(typeName, derivedName string) string { 161 return strings.Replace(derivedName, "-", "", -1) + strings.ToLower(typeName) 162 } 163 164 // discoverTypesInPath searches for types of typeID in path using go's ast and 165 // returns a list of plugins it finds. 166 func discoverTypesInPath(path, typeID, typeName string) ([]plugin, error) { 167 pluginTypes := []plugin{} 168 169 dirs, err := listDirectories(path) 170 if err != nil { 171 return pluginTypes, err 172 } 173 174 for _, dir := range dirs { 175 fset := token.NewFileSet() 176 goPackages, err := parser.ParseDir(fset, dir, nil, parser.AllErrors) 177 if err != nil { 178 return pluginTypes, fmt.Errorf("Failed parsing directory %s: %s", dir, err) 179 } 180 181 for _, goPackage := range goPackages { 182 ast.PackageExports(goPackage) 183 ast.Inspect(goPackage, func(n ast.Node) bool { 184 switch x := n.(type) { 185 case *ast.FuncDecl: 186 // If we get a function then we will check the function name 187 // against typeName and the function return type (Results) 188 // against typeID. 189 // 190 // There may be more than one return type but in the target 191 // case there should only be one. Also the return type is a 192 // ast.SelectorExpr which means we have multiple nodes. 193 // We'll read all of them as ast.Ident (identifier), join 194 // them via . to get a string like terraform.ResourceProvider 195 // and see if it matches our expected typeID 196 // 197 // This is somewhat verbose but prevents us from identifying 198 // the wrong types if the function name is amiguous or if 199 // there are other subfolders added later. 200 if x.Name.Name == typeName && len(x.Type.Results.List) == 1 { 201 node := x.Type.Results.List[0].Type 202 typeIdentifiers := []string{} 203 ast.Inspect(node, func(m ast.Node) bool { 204 switch y := m.(type) { 205 case *ast.Ident: 206 typeIdentifiers = append(typeIdentifiers, y.Name) 207 } 208 // We need all of the identifiers to join so we 209 // can't break early here. 210 return true 211 }) 212 if strings.Join(typeIdentifiers, ".") == typeID { 213 derivedName := deriveName(path, dir) 214 pluginTypes = append(pluginTypes, plugin{ 215 Package: goPackage.Name, 216 PluginName: derivedName, 217 ImportName: deriveImport(x.Name.Name, derivedName), 218 TypeName: x.Name.Name, 219 Path: dir, 220 }) 221 } 222 } 223 case *ast.TypeSpec: 224 // In the simpler case we will simply check whether the type 225 // declaration has the name we were looking for. 226 if x.Name.Name == typeID { 227 derivedName := deriveName(path, dir) 228 pluginTypes = append(pluginTypes, plugin{ 229 Package: goPackage.Name, 230 PluginName: derivedName, 231 ImportName: deriveImport(x.Name.Name, derivedName), 232 TypeName: x.Name.Name, 233 Path: dir, 234 }) 235 // The AST stops parsing when we return false. Once we 236 // find the symbol we want we can stop parsing. 237 return false 238 } 239 } 240 return true 241 }) 242 } 243 } 244 245 return pluginTypes, nil 246 } 247 248 func discoverProviders() ([]plugin, error) { 249 path := "./builtin/providers" 250 typeID := "terraform.ResourceProvider" 251 typeName := "Provider" 252 return discoverTypesInPath(path, typeID, typeName) 253 } 254 255 func discoverProvisioners() ([]plugin, error) { 256 path := "./builtin/provisioners" 257 typeID := "terraform.ResourceProvisioner" 258 typeName := "Provisioner" 259 return discoverTypesInPath(path, typeID, typeName) 260 } 261 262 const source = `// +build !core 263 264 // 265 // This file is automatically generated by scripts/generate-plugins.go -- Do not edit! 266 // 267 package command 268 269 import ( 270 IMPORTS 271 "github.com/hashicorp/terraform/plugin" 272 "github.com/hashicorp/terraform/terraform" 273 274 // Legacy, will remove once it conforms with new structure 275 chefprovisioner "github.com/hashicorp/terraform/builtin/provisioners/chef" 276 ) 277 278 var InternalProviders = map[string]plugin.ProviderFunc{ 279 PROVIDERS 280 } 281 282 var InternalProvisioners = map[string]plugin.ProvisionerFunc{ 283 PROVISIONERS 284 } 285 286 func init() { 287 // Legacy provisioners that don't match our heuristics for auto-finding 288 // built-in provisioners. 289 InternalProvisioners["chef"] = func() terraform.ResourceProvisioner { return new(chefprovisioner.ResourceProvisioner) } 290 } 291 292 `