packer-cn/scripts/generate-plugins.go

345 lines
9.2 KiB
Go

// Generate Plugins is a small program that updates the lists of plugins in
// command/plugin.go so they will be compiled into the main packer binary.
//
// See https://github.com/hashicorp/packer/pull/2608 for details.
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"io/ioutil"
"log"
"os"
"path/filepath"
"sort"
"strings"
"golang.org/x/tools/imports"
)
const target = "command/plugin.go"
func main() {
wd, _ := os.Getwd()
if filepath.Base(wd) != "packer" {
log.Fatalf("This program must be invoked in the packer project root; in %s", wd)
}
// Collect all of the data we need about plugins we have in the project
builders, err := discoverBuilders()
if err != nil {
log.Fatalf("Failed to discover builders: %s", err)
}
provisioners, err := discoverProvisioners()
if err != nil {
log.Fatalf("Failed to discover provisioners: %s", err)
}
postProcessors, err := discoverPostProcessors()
if err != nil {
log.Fatalf("Failed to discover post processors: %s", err)
}
// Do some simple code generation and templating
output := source
output = strings.Replace(output, "IMPORTS", makeImports(builders, provisioners, postProcessors), 1)
output = strings.Replace(output, "BUILDERS", makeMap("Builders", "Builder", builders), 1)
output = strings.Replace(output, "PROVISIONERS", makeMap("Provisioners", "Provisioner", provisioners), 1)
output = strings.Replace(output, "POSTPROCESSORS", makeMap("PostProcessors", "PostProcessor", postProcessors), 1)
// TODO sort the lists of plugins so we are not subjected to random OS ordering of the plugin lists
// TODO format the file
// Write our generated code to the command/plugin.go file
file, err := os.Create(target)
if err != nil {
log.Fatalf("Failed to open %s for writing: %s", target, err)
}
defer file.Close()
output = string(goFmt(target, []byte(output)))
_, err = file.WriteString(output)
if err != nil {
log.Fatalf("Failed writing to %s: %s", target, err)
}
log.Printf("Generated %s", target)
}
func goFmt(filename string, b []byte) []byte {
fb, err := imports.Process(filename, b, nil)
if err != nil {
log.Printf("formatting err: %v", err)
return b
}
return fb
}
type plugin struct {
Package string // This plugin's package name (iso)
PluginName string // Name of plugin (vmware-iso)
TypeName string // Type of plugin (builder)
Path string // Path relative to packer root (builder/vmware/iso)
ImportName string // PluginName+TypeName (vmwareisobuilder)
}
// makeMap creates a map named Name with type packer.Name that looks something
// like this:
//
// var Builders = map[string]packersdk.Builder{
// "amazon-chroot": new(chroot.Builder),
// "amazon-ebs": new(ebs.Builder),
// "amazon-instance": new(instance.Builder),
func makeMap(varName, varType string, items []plugin) string {
output := ""
output += fmt.Sprintf("var %s = map[string]packersdk.%s{\n", varName, varType)
for _, item := range items {
output += fmt.Sprintf("\t\"%s\": new(%s.%s),\n", item.PluginName, item.ImportName, item.TypeName)
}
output += "}\n"
return output
}
func makeImports(builders, provisioners, postProcessors []plugin) string {
plugins := []string{}
for _, builder := range builders {
plugins = append(plugins, fmt.Sprintf("\t%s \"github.com/hashicorp/packer/%s\"\n", builder.ImportName, filepath.ToSlash(builder.Path)))
}
for _, provisioner := range provisioners {
plugins = append(plugins, fmt.Sprintf("\t%s \"github.com/hashicorp/packer/%s\"\n", provisioner.ImportName, filepath.ToSlash(provisioner.Path)))
}
for _, postProcessor := range postProcessors {
plugins = append(plugins, fmt.Sprintf("\t%s \"github.com/hashicorp/packer/%s\"\n", postProcessor.ImportName, filepath.ToSlash(postProcessor.Path)))
}
// Make things pretty
sort.Strings(plugins)
return strings.Join(plugins, "")
}
// listDirectories recursively lists directories under the specified path
func listDirectories(path string) ([]string, error) {
names := []string{}
items, err := ioutil.ReadDir(path)
if err != nil {
return names, err
}
for _, item := range items {
// We only want directories
if !item.IsDir() ||
item.Name() == "common" {
continue
}
currentDir := filepath.Join(path, item.Name())
names = append(names, currentDir)
// Do some recursion
subNames, err := listDirectories(currentDir)
if err == nil {
names = append(names, subNames...)
}
}
return names, nil
}
// deriveName determines the name of the plugin (what you'll see in a packer
// template) based on the filesystem path. We use two rules:
//
// Start with -> builder/virtualbox/iso
//
// 1. Strip the root -> virtualbox/iso
// 2. Switch slash / to dash - -> virtualbox-iso
func deriveName(root, full string) string {
short, _ := filepath.Rel(root, full)
bits := strings.Split(short, string(os.PathSeparator))
return strings.Join(bits, "-")
}
// deriveImport will build a unique import identifier based on packageName and
// the result of deriveName()
//
// This will be something like -> virtualboxisobuilder
//
// Which is long, but deterministic and unique.
func deriveImport(typeName, derivedName string) string {
return strings.Replace(derivedName, "-", "", -1) + strings.ToLower(typeName)
}
// discoverTypesInPath searches for types of typeID in path and returns a list
// of plugins it finds.
func discoverTypesInPath(path, typeID string) ([]plugin, error) {
postProcessors := []plugin{}
dirs, err := listDirectories(path)
if err != nil {
return postProcessors, err
}
for _, dir := range dirs {
fset := token.NewFileSet()
goPackages, err := parser.ParseDir(fset, dir, nil, parser.AllErrors)
if err != nil {
return postProcessors, fmt.Errorf("Failed parsing directory %s: %s", dir, err)
}
for _, goPackage := range goPackages {
ast.PackageExports(goPackage)
ast.Inspect(goPackage, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.TypeSpec:
if x.Name.Name == typeID {
derivedName := deriveName(path, dir)
postProcessors = append(postProcessors, plugin{
Package: goPackage.Name,
PluginName: derivedName,
ImportName: deriveImport(x.Name.Name, derivedName),
TypeName: x.Name.Name,
Path: dir,
})
// The AST stops parsing when we return false. Once we
// find the symbol we want we can stop parsing.
// DEBUG:
// fmt.Printf("package %#v\n", goPackage)
return false
}
}
return true
})
}
}
return postProcessors, nil
}
func discoverBuilders() ([]plugin, error) {
path := "./builder"
typeID := "Builder"
return discoverTypesInPath(path, typeID)
}
func discoverProvisioners() ([]plugin, error) {
path := "./provisioner"
typeID := "Provisioner"
return discoverTypesInPath(path, typeID)
}
func discoverPostProcessors() ([]plugin, error) {
path := "./post-processor"
typeID := "PostProcessor"
return discoverTypesInPath(path, typeID)
}
const source = `//
// This file is automatically generated by scripts/generate-plugins.go -- Do not edit!
//
package command
import (
"fmt"
"log"
"regexp"
"strings"
"github.com/hashicorp/packer/packer"
packersdk "github.com/hashicorp/packer/packer-plugin-sdk/packer"
"github.com/hashicorp/packer/packer-plugin-sdk/plugin"
IMPORTS
)
type PluginCommand struct {
Meta
}
BUILDERS
PROVISIONERS
POSTPROCESSORS
var pluginRegexp = regexp.MustCompile("packer-(builder|post-processor|provisioner)-(.+)")
func (c *PluginCommand) Run(args []string) int {
// This is an internal call (users should not call this directly) so we're
// not going to do much input validation. If there's a problem we'll often
// just crash. Error handling should be added to facilitate debugging.
log.Printf("args: %#v", args)
if len(args) != 1 {
c.Ui.Error("Wrong number of args")
return 1
}
// Plugin will match something like "packer-builder-amazon-ebs"
parts := pluginRegexp.FindStringSubmatch(args[0])
if len(parts) != 3 {
c.Ui.Error(fmt.Sprintf("Error parsing plugin argument [DEBUG]: %#v", parts))
return 1
}
pluginType := parts[1] // capture group 1 (builder|post-processor|provisioner)
pluginName := parts[2] // capture group 2 (.+)
server, err := plugin.Server()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error starting plugin server: %s", err))
return 1
}
switch pluginType {
case "builder":
builder, found := Builders[pluginName]
if !found {
c.Ui.Error(fmt.Sprintf("Could not load builder: %s", pluginName))
return 1
}
server.RegisterBuilder(builder)
case "provisioner":
provisioner, found := Provisioners[pluginName]
if !found {
c.Ui.Error(fmt.Sprintf("Could not load provisioner: %s", pluginName))
return 1
}
server.RegisterProvisioner(provisioner)
case "post-processor":
postProcessor, found := PostProcessors[pluginName]
if !found {
c.Ui.Error(fmt.Sprintf("Could not load post-processor: %s", pluginName))
return 1
}
server.RegisterPostProcessor(postProcessor)
}
server.Serve()
return 0
}
func (*PluginCommand) Help() string {
helpText := ` + "`" + `
Usage: packer plugin PLUGIN
Runs an internally-compiled version of a plugin from the packer binary.
NOTE: this is an internal command and you should not call it yourself.
` + "`" + `
return strings.TrimSpace(helpText)
}
func (c *PluginCommand) Synopsis() string {
return "internal plugin command"
}
`