github.com/hashicorp/packer@v1.14.3/scripts/generate-plugins.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  // Generate Plugins is a small program that updates the lists of plugins in
     5  // command/plugin.go so they will be compiled into the main packer binary.
     6  //
     7  // See https://github.com/hashicorp/packer/pull/2608 for details.
     8  package main
     9  
    10  import (
    11  	"fmt"
    12  	"go/ast"
    13  	"go/parser"
    14  	"go/token"
    15  	"log"
    16  	"os"
    17  	"path/filepath"
    18  	"sort"
    19  	"strings"
    20  
    21  	"golang.org/x/tools/imports"
    22  )
    23  
    24  const target = "command/execute.go"
    25  
    26  func main() {
    27  	wd, _ := os.Getwd()
    28  	if filepath.Base(wd) != "packer" {
    29  		log.Fatalf("This program must be invoked in the packer project root; in %s", wd)
    30  	}
    31  
    32  	// Collect all of the data we need about plugins we have in the project
    33  	builders, err := discoverBuilders()
    34  	if err != nil {
    35  		log.Fatalf("Failed to discover builders: %s", err)
    36  	}
    37  
    38  	provisioners, err := discoverProvisioners()
    39  	if err != nil {
    40  		log.Fatalf("Failed to discover provisioners: %s", err)
    41  	}
    42  
    43  	postProcessors, err := discoverPostProcessors()
    44  	if err != nil {
    45  		log.Fatalf("Failed to discover post processors: %s", err)
    46  	}
    47  
    48  	datasources, err := discoverDatasources()
    49  	if err != nil {
    50  		log.Fatalf("Failed to discover Datasources: %s", err)
    51  	}
    52  
    53  	// Do some simple code generation and templating
    54  	output := source
    55  	output = strings.Replace(output, "IMPORTS", makeImports(builders, provisioners, postProcessors, datasources), 1)
    56  	output = strings.Replace(output, "BUILDERS", makeMap("Builders", "Builder", builders), 1)
    57  	output = strings.Replace(output, "PROVISIONERS", makeMap("Provisioners", "Provisioner", provisioners), 1)
    58  	output = strings.Replace(output, "POSTPROCESSORS", makeMap("PostProcessors", "PostProcessor", postProcessors), 1)
    59  	output = strings.Replace(output, "DATASOURCES", makeMap("Datasources", "Datasource", datasources), 1)
    60  
    61  	// TODO sort the lists of plugins so we are not subjected to random OS ordering of the plugin lists
    62  	// TODO format the file
    63  
    64  	// Write our generated code to the command/plugin.go file
    65  	file, err := os.Create(target)
    66  	if err != nil {
    67  		log.Fatalf("Failed to open %s for writing: %s", target, err)
    68  	}
    69  	defer file.Close()
    70  
    71  	output = string(goFmt(target, []byte(output)))
    72  
    73  	_, err = file.WriteString(output)
    74  	if err != nil {
    75  		log.Fatalf("Failed writing to %s: %s", target, err)
    76  	}
    77  
    78  	log.Printf("Generated %s", target)
    79  }
    80  
    81  func goFmt(filename string, b []byte) []byte {
    82  	fb, err := imports.Process(filename, b, nil)
    83  	if err != nil {
    84  		log.Printf("formatting err: %v", err)
    85  		return b
    86  	}
    87  	return fb
    88  }
    89  
    90  type plugin struct {
    91  	Package    string // This plugin's package name (iso)
    92  	PluginName string // Name of plugin (vmware-iso)
    93  	TypeName   string // Type of plugin (builder)
    94  	Path       string // Path relative to packer root (builder/vmware/iso)
    95  	ImportName string // PluginName+TypeName (vmwareisobuilder)
    96  }
    97  
    98  // makeMap creates a map named Name with type packer.Name that looks something
    99  // like this:
   100  //
   101  //	var Builders = map[string]packersdk.Builder{
   102  //		"amazon-chroot":   new(chroot.Builder),
   103  //		"amazon-ebs":      new(ebs.Builder),
   104  //		"amazon-instance": new(instance.Builder),
   105  func makeMap(varName, varType string, items []plugin) string {
   106  	output := ""
   107  
   108  	output += fmt.Sprintf("var %s = map[string]packersdk.%s{\n", varName, varType)
   109  	for _, item := range items {
   110  		output += fmt.Sprintf("\t\"%s\":   new(%s.%s),\n", item.PluginName, item.ImportName, item.TypeName)
   111  	}
   112  	output += "}\n"
   113  	return output
   114  }
   115  
   116  func makeImports(builders, provisioners, postProcessors, Datasources []plugin) string {
   117  	plugins := []string{}
   118  
   119  	for _, builder := range builders {
   120  		plugins = append(plugins, fmt.Sprintf("\t%s \"github.com/hashicorp/packer/%s\"\n", builder.ImportName, filepath.ToSlash(builder.Path)))
   121  	}
   122  
   123  	for _, provisioner := range provisioners {
   124  		plugins = append(plugins, fmt.Sprintf("\t%s \"github.com/hashicorp/packer/%s\"\n", provisioner.ImportName, filepath.ToSlash(provisioner.Path)))
   125  	}
   126  
   127  	for _, postProcessor := range postProcessors {
   128  		plugins = append(plugins, fmt.Sprintf("\t%s \"github.com/hashicorp/packer/%s\"\n", postProcessor.ImportName, filepath.ToSlash(postProcessor.Path)))
   129  	}
   130  
   131  	for _, datasource := range Datasources {
   132  		plugins = append(plugins, fmt.Sprintf("\t%s \"github.com/hashicorp/packer/%s\"\n", datasource.ImportName, filepath.ToSlash(datasource.Path)))
   133  	}
   134  
   135  	// Make things pretty
   136  	sort.Strings(plugins)
   137  
   138  	return strings.Join(plugins, "")
   139  }
   140  
   141  // listDirectories recursively lists directories under the specified path
   142  func listDirectories(path string) ([]string, error) {
   143  	names := []string{}
   144  	items, err := os.ReadDir(path)
   145  	if err != nil {
   146  		return names, err
   147  	}
   148  
   149  	for _, item := range items {
   150  		// We only want directories
   151  		if !item.IsDir() ||
   152  			item.Name() == "common" {
   153  			continue
   154  		}
   155  		currentDir := filepath.Join(path, item.Name())
   156  		names = append(names, currentDir)
   157  
   158  		// Do some recursion
   159  		subNames, err := listDirectories(currentDir)
   160  		if err == nil {
   161  			names = append(names, subNames...)
   162  		}
   163  	}
   164  
   165  	return names, nil
   166  }
   167  
   168  // deriveName determines the name of the plugin (what you'll see in a packer
   169  // template) based on the filesystem path. We use two rules:
   170  //
   171  // Start with                     -> builder/virtualbox/iso
   172  //
   173  // 1. Strip the root              -> virtualbox/iso
   174  // 2. Switch slash / to dash -    -> virtualbox-iso
   175  func deriveName(root, full string) string {
   176  	short, _ := filepath.Rel(root, full)
   177  	bits := strings.Split(short, string(os.PathSeparator))
   178  	return strings.Join(bits, "-")
   179  }
   180  
   181  // deriveImport will build a unique import identifier based on packageName and
   182  // the result of deriveName()
   183  //
   184  // This will be something like    -> virtualboxisobuilder
   185  //
   186  // Which is long, but deterministic and unique.
   187  func deriveImport(typeName, derivedName string) string {
   188  	return strings.Replace(derivedName, "-", "", -1) + strings.ToLower(typeName)
   189  }
   190  
   191  // discoverTypesInPath searches for types of typeID in path and returns a list
   192  // of plugins it finds.
   193  func discoverTypesInPath(path, typeID string) ([]plugin, error) {
   194  	postProcessors := []plugin{}
   195  
   196  	dirs, err := listDirectories(path)
   197  	if err != nil {
   198  		return postProcessors, err
   199  	}
   200  
   201  	for _, dir := range dirs {
   202  		fset := token.NewFileSet()
   203  		goPackages, err := parser.ParseDir(fset, dir, nil, parser.AllErrors)
   204  		if err != nil {
   205  			return postProcessors, fmt.Errorf("Failed parsing directory %s: %s", dir, err)
   206  		}
   207  
   208  		for _, goPackage := range goPackages {
   209  			ast.PackageExports(goPackage)
   210  			ast.Inspect(goPackage, func(n ast.Node) bool {
   211  				switch x := n.(type) {
   212  				case *ast.TypeSpec:
   213  					if x.Name.Name == typeID {
   214  						derivedName := deriveName(path, dir)
   215  						postProcessors = append(postProcessors, plugin{
   216  							Package:    goPackage.Name,
   217  							PluginName: derivedName,
   218  							ImportName: deriveImport(x.Name.Name, derivedName),
   219  							TypeName:   x.Name.Name,
   220  							Path:       dir,
   221  						})
   222  						// The AST stops parsing when we return false. Once we
   223  						// find the symbol we want we can stop parsing.
   224  
   225  						// DEBUG:
   226  						// fmt.Printf("package %#v\n", goPackage)
   227  						return false
   228  					}
   229  				}
   230  				return true
   231  			})
   232  		}
   233  	}
   234  
   235  	return postProcessors, nil
   236  }
   237  
   238  func discoverBuilders() ([]plugin, error) {
   239  	path := "./builder"
   240  	typeID := "Builder"
   241  	return discoverTypesInPath(path, typeID)
   242  }
   243  
   244  func discoverDatasources() ([]plugin, error) {
   245  	path := "./datasource"
   246  	typeID := "Datasource"
   247  	return discoverTypesInPath(path, typeID)
   248  }
   249  
   250  func discoverProvisioners() ([]plugin, error) {
   251  	path := "./provisioner"
   252  	typeID := "Provisioner"
   253  	return discoverTypesInPath(path, typeID)
   254  }
   255  
   256  func discoverPostProcessors() ([]plugin, error) {
   257  	path := "./post-processor"
   258  	typeID := "PostProcessor"
   259  	return discoverTypesInPath(path, typeID)
   260  }
   261  
   262  const source = `//
   263  // This file is automatically generated by scripts/generate-plugins.go -- Do not edit!
   264  //
   265  
   266  package command
   267  
   268  import (
   269  	"flag"
   270  	"fmt"
   271  	"regexp"
   272  	"strings"
   273  
   274  	"github.com/hashicorp/packer/packer"
   275  packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
   276  	"github.com/hashicorp/packer-plugin-sdk/plugin"
   277  	"github.com/hashicorp/packer-plugin-sdk/rpc"
   278  
   279  IMPORTS
   280  )
   281  
   282  type ExecuteCommand struct {
   283  	Meta
   284  }
   285  
   286  BUILDERS
   287  
   288  PROVISIONERS
   289  
   290  POSTPROCESSORS
   291  
   292  DATASOURCES
   293  
   294  var pluginRegexp = regexp.MustCompile("packer-(builder|post-processor|provisioner|datasource)-(.+)")
   295  
   296  type ExecuteArgs struct {
   297  	UseProtobuf bool
   298  	CommandType string
   299  }
   300  
   301  func (ea *ExecuteArgs) AddFlagSets(flags *flag.FlagSet) {
   302  	flags.BoolVar(&ea.UseProtobuf, "protobuf", false, "Use protobuf for serialising data over the wire instead of gob")
   303  }
   304  
   305  func (c *ExecuteCommand) ParseArgs(args []string) (*ExecuteArgs, int) {
   306  	var cfg ExecuteArgs
   307  	flags := c.Meta.FlagSet("")
   308  	flags.Usage = func() { c.Ui.Say(c.Help()) }
   309  	cfg.AddFlagSets(flags)
   310  	if err := flags.Parse(args); err != nil {
   311  		return &cfg, 1
   312  	}
   313  
   314  	args = flags.Args()
   315  	if len(args) != 1 {
   316  		flags.Usage()
   317  		return &cfg, 1
   318  	}
   319  	cfg.CommandType = args[0]
   320  	return &cfg, 0
   321  }
   322  
   323  func (c *ExecuteCommand) Run(args []string) int {
   324  	cfg, ret := c.ParseArgs(args)
   325  	if ret != 0 {
   326  		return ret
   327  	}
   328  
   329  	return c.RunContext(cfg)
   330  }
   331  
   332  
   333  func (c *ExecuteCommand) RunContext(args *ExecuteArgs) int {
   334  	// Plugin will match something like "packer-builder-amazon-ebs"
   335  	parts := pluginRegexp.FindStringSubmatch(args.CommandType)
   336  	if len(parts) != 3 {
   337  		c.Ui.Error(c.Help())
   338  		return 1
   339  	}
   340  	pluginType := parts[1] // capture group 1 (builder|post-processor|provisioner)
   341  	pluginName := parts[2] // capture group 2 (.+)
   342  
   343  	server, err := plugin.Server()
   344  	if err != nil {
   345  		c.Ui.Error(fmt.Sprintf("Error starting plugin server: %s", err))
   346  		return 1
   347  	}
   348  
   349  	if args.UseProtobuf {
   350  		server.UseProto = true
   351  	}
   352  
   353  	switch pluginType {
   354  	case "builder":
   355  		builder, found := Builders[pluginName]
   356  		if !found {
   357  			c.Ui.Error(fmt.Sprintf("Could not load builder: %s", pluginName))
   358  			return 1
   359  		}
   360  		server.RegisterBuilder(builder)
   361  	case "provisioner":
   362  		provisioner, found := Provisioners[pluginName]
   363  		if !found {
   364  			c.Ui.Error(fmt.Sprintf("Could not load provisioner: %s", pluginName))
   365  			return 1
   366  		}
   367  		server.RegisterProvisioner(provisioner)
   368  	case "post-processor":
   369  		postProcessor, found := PostProcessors[pluginName]
   370  		if !found {
   371  			c.Ui.Error(fmt.Sprintf("Could not load post-processor: %s", pluginName))
   372  			return 1
   373  		}
   374  		server.RegisterPostProcessor(postProcessor)
   375  	case "datasource":
   376  		datasource, found := Datasources[pluginName]
   377  		if !found {
   378  			c.Ui.Error(fmt.Sprintf("Could not load datasource: %s", pluginName))
   379  			return 1
   380  		}
   381  		server.RegisterDatasource(datasource)
   382  	}
   383  
   384  	server.Serve()
   385  
   386  	return 0
   387  }
   388  
   389  func (*ExecuteCommand) Help() string {
   390  	helpText := ` + "`" + `
   391  Usage: packer execute [options] PLUGIN
   392  
   393    Runs an internally-compiled version of a plugin from the packer binary.
   394  
   395    NOTE: this is an internal command and you should not call it yourself.
   396  
   397  Options:
   398  
   399    --protobuf: use protobuf for serialising data over-the-wire instead of gob.
   400  ` + "`" + `
   401  
   402  	return strings.TrimSpace(helpText)
   403  }
   404  
   405  func (c *ExecuteCommand) Synopsis() string {
   406  	return "internal plugin command"
   407  }
   408  `