github.com/ticketmaster/terraform@v0.10.0-beta2.0.20170711045249-a12daf5aba4f/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(nil, 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 // "chef": chefprovisioner.Provisioner, 86 // "file": fileprovisioner.Provisioner, 87 // "local-exec": localexecprovisioner.Provisioner, 88 // "remote-exec": remoteexecprovisioner.Provisioner, 89 // 90 func makeProvisionerMap(items []plugin) string { 91 output := "" 92 for _, item := range items { 93 output += fmt.Sprintf("\t\"%s\": %s.%s,\n", item.PluginName, item.ImportName, item.TypeName) 94 } 95 return output 96 } 97 98 func makeImports(providers, provisioners []plugin) string { 99 plugins := []string{} 100 101 for _, provider := range providers { 102 plugins = append(plugins, fmt.Sprintf("\t%s \"github.com/hashicorp/terraform/%s\"\n", provider.ImportName, filepath.ToSlash(provider.Path))) 103 } 104 105 for _, provisioner := range provisioners { 106 plugins = append(plugins, fmt.Sprintf("\t%s \"github.com/hashicorp/terraform/%s\"\n", provisioner.ImportName, filepath.ToSlash(provisioner.Path))) 107 } 108 109 // Make things pretty 110 sort.Strings(plugins) 111 112 return strings.Join(plugins, "") 113 } 114 115 // listDirectories recursively lists directories under the specified path 116 func listDirectories(path string) ([]string, error) { 117 names := []string{} 118 items, err := ioutil.ReadDir(path) 119 if err != nil { 120 return names, err 121 } 122 123 for _, item := range items { 124 // We only want directories 125 if item.IsDir() { 126 if item.Name() == "test-fixtures" { 127 continue 128 } 129 currentDir := filepath.Join(path, item.Name()) 130 names = append(names, currentDir) 131 132 // Do some recursion 133 subNames, err := listDirectories(currentDir) 134 if err == nil { 135 names = append(names, subNames...) 136 } 137 } 138 } 139 140 return names, nil 141 } 142 143 // deriveName determines the name of the plugin relative to the specified root 144 // path. 145 func deriveName(root, full string) string { 146 short, _ := filepath.Rel(root, full) 147 bits := strings.Split(short, string(os.PathSeparator)) 148 return strings.Join(bits, "-") 149 } 150 151 // deriveImport will build a unique import identifier based on packageName and 152 // the result of deriveName(). This is important for disambigutating between 153 // providers and provisioners that have the same name. This will be something 154 // like: 155 // 156 // remote-exec -> remoteexecprovisioner 157 // 158 // which is long, but is deterministic and unique. 159 func deriveImport(typeName, derivedName string) string { 160 return strings.Replace(derivedName, "-", "", -1) + strings.ToLower(typeName) 161 } 162 163 // discoverTypesInPath searches for types of typeID in path using go's ast and 164 // returns a list of plugins it finds. 165 func discoverTypesInPath(path, typeID, typeName string) ([]plugin, error) { 166 pluginTypes := []plugin{} 167 168 dirs, err := listDirectories(path) 169 if err != nil { 170 return pluginTypes, err 171 } 172 173 for _, dir := range dirs { 174 fset := token.NewFileSet() 175 goPackages, err := parser.ParseDir(fset, dir, nil, parser.AllErrors) 176 if err != nil { 177 return pluginTypes, fmt.Errorf("Failed parsing directory %s: %s", dir, err) 178 } 179 180 for _, goPackage := range goPackages { 181 ast.PackageExports(goPackage) 182 ast.Inspect(goPackage, func(n ast.Node) bool { 183 switch x := n.(type) { 184 case *ast.FuncDecl: 185 // If we get a function then we will check the function name 186 // against typeName and the function return type (Results) 187 // against typeID. 188 // 189 // There may be more than one return type but in the target 190 // case there should only be one. Also the return type is a 191 // ast.SelectorExpr which means we have multiple nodes. 192 // We'll read all of them as ast.Ident (identifier), join 193 // them via . to get a string like terraform.ResourceProvider 194 // and see if it matches our expected typeID 195 // 196 // This is somewhat verbose but prevents us from identifying 197 // the wrong types if the function name is amiguous or if 198 // there are other subfolders added later. 199 if x.Name.Name == typeName && len(x.Type.Results.List) == 1 { 200 node := x.Type.Results.List[0].Type 201 typeIdentifiers := []string{} 202 ast.Inspect(node, func(m ast.Node) bool { 203 switch y := m.(type) { 204 case *ast.Ident: 205 typeIdentifiers = append(typeIdentifiers, y.Name) 206 } 207 // We need all of the identifiers to join so we 208 // can't break early here. 209 return true 210 }) 211 if strings.Join(typeIdentifiers, ".") == typeID { 212 derivedName := deriveName(path, dir) 213 pluginTypes = append(pluginTypes, plugin{ 214 Package: goPackage.Name, 215 PluginName: derivedName, 216 ImportName: deriveImport(x.Name.Name, derivedName), 217 TypeName: x.Name.Name, 218 Path: dir, 219 }) 220 } 221 } 222 case *ast.TypeSpec: 223 // In the simpler case we will simply check whether the type 224 // declaration has the name we were looking for. 225 if x.Name.Name == typeID { 226 derivedName := deriveName(path, dir) 227 pluginTypes = append(pluginTypes, plugin{ 228 Package: goPackage.Name, 229 PluginName: derivedName, 230 ImportName: deriveImport(x.Name.Name, derivedName), 231 TypeName: x.Name.Name, 232 Path: dir, 233 }) 234 // The AST stops parsing when we return false. Once we 235 // find the symbol we want we can stop parsing. 236 return false 237 } 238 } 239 return true 240 }) 241 } 242 } 243 244 return pluginTypes, nil 245 } 246 247 func discoverProviders() ([]plugin, error) { 248 path := "./builtin/providers" 249 typeID := "terraform.ResourceProvider" 250 typeName := "Provider" 251 return discoverTypesInPath(path, typeID, typeName) 252 } 253 254 func discoverProvisioners() ([]plugin, error) { 255 path := "./builtin/provisioners" 256 typeID := "terraform.ResourceProvisioner" 257 typeName := "Provisioner" 258 return discoverTypesInPath(path, typeID, typeName) 259 } 260 261 const source = `// 262 // This file is automatically generated by scripts/generate-plugins.go -- Do not edit! 263 // 264 package command 265 266 import ( 267 IMPORTS 268 "github.com/hashicorp/terraform/plugin" 269 ) 270 271 var InternalProviders = map[string]plugin.ProviderFunc{} 272 273 var InternalProvisioners = map[string]plugin.ProvisionerFunc{ 274 PROVISIONERS 275 } 276 `