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  `