diff --git a/Makefile b/Makefile index eb3157d14..a86936fa8 100644 --- a/Makefile +++ b/Makefile @@ -20,10 +20,11 @@ quickdev: generate # target should be used. core-dev: generate go install github.com/hashicorp/terraform + go install -tags 'core' github.com/hashicorp/terraform # Shorthand for quickly testing the core of Terraform (i.e. "not providers") core-test: generate - @echo "Testing core packages..." && go test $(shell go list ./... | grep -v -E 'builtin|vendor') + @echo "Testing core packages..." && go test -tags 'core' $(shell go list ./... | grep -v -E 'builtin|vendor') # Shorthand for building and installing just one plugin for local testing. # Run as (for example): make plugin-dev PLUGIN=provider-aws @@ -77,6 +78,7 @@ generate: go get -u golang.org/x/tools/cmd/stringer; \ fi go generate $$(go list ./... | grep -v /vendor/) + @go fmt command/internal_plugin_list.go > /dev/null fmt: gofmt -w . diff --git a/command/internal_plugin.go b/command/internal_plugin.go new file mode 100644 index 000000000..1e027cfdb --- /dev/null +++ b/command/internal_plugin.go @@ -0,0 +1,87 @@ +package command + +import ( + "log" + "strings" + + "github.com/hashicorp/terraform/plugin" + "github.com/kardianos/osext" +) + +// InternalPluginCommand is a Command implementation that allows plugins to be +// compiled into the main Terraform binary and executed via a subcommand. +type InternalPluginCommand struct { + Meta +} + +const TFSPACE = "-TFSPACE-" + +// BuildPluginCommandString builds a special string for executing internal +// plugins. It has the following format: +// +// /path/to/terraform-TFSPACE-internal-plugin-TFSPACE-terraform-provider-aws +// +// We split the string on -TFSPACE- to build the command executor. The reason we +// use -TFSPACE- is so we can support spaces in the /path/to/terraform part. +func BuildPluginCommandString(pluginType, pluginName string) (string, error) { + terraformPath, err := osext.Executable() + if err != nil { + return "", err + } + parts := []string{terraformPath, "internal-plugin", pluginType, pluginName} + return strings.Join(parts, TFSPACE), nil +} + +func (c *InternalPluginCommand) Run(args []string) int { + if len(args) != 2 { + log.Printf("Wrong number of args; expected: terraform internal-plugin pluginType pluginName") + return 1 + } + + pluginType := args[0] + pluginName := args[1] + + switch pluginType { + case "provider": + pluginFunc, found := InternalProviders[pluginName] + if !found { + log.Printf("[ERROR] Could not load provider: %s", pluginName) + return 1 + } + log.Printf("[INFO] Starting provider plugin %s", pluginName) + plugin.Serve(&plugin.ServeOpts{ + ProviderFunc: pluginFunc, + }) + case "provisioner": + pluginFunc, found := InternalProvisioners[pluginName] + if !found { + log.Printf("[ERROR] Could not load provisioner: %s", pluginName) + return 1 + } + log.Printf("[INFO] Starting provisioner plugin %s", pluginName) + plugin.Serve(&plugin.ServeOpts{ + ProvisionerFunc: pluginFunc, + }) + default: + log.Printf("[ERROR] Invalid plugin type %s", pluginType) + return 1 + } + + return 0 +} + +func (c *InternalPluginCommand) Help() string { + helpText := ` +Usage: terraform internal-plugin pluginType pluginName + + Runs an internally-compiled version of a plugin from the terraform binary. + + NOTE: this is an internal command and you should not call it yourself. +` + + return strings.TrimSpace(helpText) +} + +func (c *InternalPluginCommand) Synopsis() string { + return "internal plugin command" +} diff --git a/command/internal_plugin_core.go b/command/internal_plugin_core.go new file mode 100644 index 000000000..e0d2c9477 --- /dev/null +++ b/command/internal_plugin_core.go @@ -0,0 +1,13 @@ +// +build core + +// This file is included whenever the 'core' build tag is specified. This is +// used by make core-dev and make core-test to compile a build significantly +// more quickly, but it will not include any provider or provisioner plugins. + +package command + +import "github.com/hashicorp/terraform/plugin" + +var InternalProviders = map[string]plugin.ProviderFunc{} + +var InternalProvisioners = map[string]plugin.ProvisionerFunc{} diff --git a/command/internal_plugin_list.go b/command/internal_plugin_list.go new file mode 100644 index 000000000..fc988fa61 --- /dev/null +++ b/command/internal_plugin_list.go @@ -0,0 +1,100 @@ +// +build !core + +// +// This file is automatically generated by scripts/generate-plugins.go -- Do not edit! +// +package command + +import ( + atlasprovider "github.com/hashicorp/terraform/builtin/providers/atlas" + awsprovider "github.com/hashicorp/terraform/builtin/providers/aws" + azureprovider "github.com/hashicorp/terraform/builtin/providers/azure" + azurermprovider "github.com/hashicorp/terraform/builtin/providers/azurerm" + chefprovider "github.com/hashicorp/terraform/builtin/providers/chef" + clcprovider "github.com/hashicorp/terraform/builtin/providers/clc" + cloudflareprovider "github.com/hashicorp/terraform/builtin/providers/cloudflare" + cloudstackprovider "github.com/hashicorp/terraform/builtin/providers/cloudstack" + consulprovider "github.com/hashicorp/terraform/builtin/providers/consul" + datadogprovider "github.com/hashicorp/terraform/builtin/providers/datadog" + digitaloceanprovider "github.com/hashicorp/terraform/builtin/providers/digitalocean" + dmeprovider "github.com/hashicorp/terraform/builtin/providers/dme" + dnsimpleprovider "github.com/hashicorp/terraform/builtin/providers/dnsimple" + dockerprovider "github.com/hashicorp/terraform/builtin/providers/docker" + dynprovider "github.com/hashicorp/terraform/builtin/providers/dyn" + fastlyprovider "github.com/hashicorp/terraform/builtin/providers/fastly" + githubprovider "github.com/hashicorp/terraform/builtin/providers/github" + googleprovider "github.com/hashicorp/terraform/builtin/providers/google" + herokuprovider "github.com/hashicorp/terraform/builtin/providers/heroku" + influxdbprovider "github.com/hashicorp/terraform/builtin/providers/influxdb" + mailgunprovider "github.com/hashicorp/terraform/builtin/providers/mailgun" + mysqlprovider "github.com/hashicorp/terraform/builtin/providers/mysql" + nullprovider "github.com/hashicorp/terraform/builtin/providers/null" + openstackprovider "github.com/hashicorp/terraform/builtin/providers/openstack" + packetprovider "github.com/hashicorp/terraform/builtin/providers/packet" + postgresqlprovider "github.com/hashicorp/terraform/builtin/providers/postgresql" + powerdnsprovider "github.com/hashicorp/terraform/builtin/providers/powerdns" + rundeckprovider "github.com/hashicorp/terraform/builtin/providers/rundeck" + statuscakeprovider "github.com/hashicorp/terraform/builtin/providers/statuscake" + templateprovider "github.com/hashicorp/terraform/builtin/providers/template" + terraformprovider "github.com/hashicorp/terraform/builtin/providers/terraform" + testprovider "github.com/hashicorp/terraform/builtin/providers/test" + tlsprovider "github.com/hashicorp/terraform/builtin/providers/tls" + tritonprovider "github.com/hashicorp/terraform/builtin/providers/triton" + ultradnsprovider "github.com/hashicorp/terraform/builtin/providers/ultradns" + vcdprovider "github.com/hashicorp/terraform/builtin/providers/vcd" + vsphereprovider "github.com/hashicorp/terraform/builtin/providers/vsphere" + chefresourceprovisioner "github.com/hashicorp/terraform/builtin/provisioners/chef" + fileresourceprovisioner "github.com/hashicorp/terraform/builtin/provisioners/file" + localexecresourceprovisioner "github.com/hashicorp/terraform/builtin/provisioners/local-exec" + remoteexecresourceprovisioner "github.com/hashicorp/terraform/builtin/provisioners/remote-exec" + + "github.com/hashicorp/terraform/plugin" + "github.com/hashicorp/terraform/terraform" +) + +var InternalProviders = map[string]plugin.ProviderFunc{ + "atlas": atlasprovider.Provider, + "aws": awsprovider.Provider, + "azure": azureprovider.Provider, + "azurerm": azurermprovider.Provider, + "chef": chefprovider.Provider, + "clc": clcprovider.Provider, + "cloudflare": cloudflareprovider.Provider, + "cloudstack": cloudstackprovider.Provider, + "consul": consulprovider.Provider, + "datadog": datadogprovider.Provider, + "digitalocean": digitaloceanprovider.Provider, + "dme": dmeprovider.Provider, + "dnsimple": dnsimpleprovider.Provider, + "docker": dockerprovider.Provider, + "dyn": dynprovider.Provider, + "fastly": fastlyprovider.Provider, + "github": githubprovider.Provider, + "google": googleprovider.Provider, + "heroku": herokuprovider.Provider, + "influxdb": influxdbprovider.Provider, + "mailgun": mailgunprovider.Provider, + "mysql": mysqlprovider.Provider, + "null": nullprovider.Provider, + "openstack": openstackprovider.Provider, + "packet": packetprovider.Provider, + "postgresql": postgresqlprovider.Provider, + "powerdns": powerdnsprovider.Provider, + "rundeck": rundeckprovider.Provider, + "statuscake": statuscakeprovider.Provider, + "template": templateprovider.Provider, + "terraform": terraformprovider.Provider, + "test": testprovider.Provider, + "tls": tlsprovider.Provider, + "triton": tritonprovider.Provider, + "ultradns": ultradnsprovider.Provider, + "vcd": vcdprovider.Provider, + "vsphere": vsphereprovider.Provider, +} + +var InternalProvisioners = map[string]plugin.ProvisionerFunc{ + "chef": func() terraform.ResourceProvisioner { return new(chefresourceprovisioner.ResourceProvisioner) }, + "file": func() terraform.ResourceProvisioner { return new(fileresourceprovisioner.ResourceProvisioner) }, + "local-exec": func() terraform.ResourceProvisioner { return new(localexecresourceprovisioner.ResourceProvisioner) }, + "remote-exec": func() terraform.ResourceProvisioner { return new(remoteexecresourceprovisioner.ResourceProvisioner) }, +} diff --git a/command/internal_plugin_test.go b/command/internal_plugin_test.go new file mode 100644 index 000000000..6a28f41dc --- /dev/null +++ b/command/internal_plugin_test.go @@ -0,0 +1,34 @@ +// +build !core + +package command + +import "testing" + +func TestInternalPlugin_InternalProviders(t *testing.T) { + // Note this is a randomish sample and does not check for all plugins + for _, name := range []string{"atlas", "consul", "docker", "template"} { + if _, ok := InternalProviders[name]; !ok { + t.Errorf("Expected to find %s in InternalProviders", name) + } + } +} + +func TestInternalPlugin_InternalProvisioners(t *testing.T) { + for _, name := range []string{"chef", "file", "local-exec", "remote-exec"} { + if _, ok := InternalProvisioners[name]; !ok { + t.Errorf("Expected to find %s in InternalProvisioners", name) + } + } +} + +func TestInternalPlugin_BuildPluginCommandString(t *testing.T) { + actual, err := BuildPluginCommandString("provisioner", "remote-exec") + if err != nil { + t.Fatalf(err.Error()) + } + + expected := "-TFSPACE-internal-plugin-TFSPACE-provisioner-TFSPACE-remote-exec" + if actual[len(actual)-len(expected):] != expected { + t.Errorf("Expected command to end with %s; got:\n%s\n", expected, actual) + } +} diff --git a/commands.go b/commands.go index 69454f2eb..290673c35 100644 --- a/commands.go +++ b/commands.go @@ -79,6 +79,12 @@ func init() { }, nil }, + "internal-plugin": func() (cli.Command, error) { + return &command.InternalPluginCommand{ + Meta: meta, + }, nil + }, + "output": func() (cli.Command, error) { return &command.OutputCommand{ Meta: meta, diff --git a/config.go b/config.go index 1d473d4ad..0fe3e314a 100644 --- a/config.go +++ b/config.go @@ -1,3 +1,4 @@ +//go:generate go run ./scripts/generate-plugins.go package main import ( @@ -11,6 +12,7 @@ import ( "github.com/hashicorp/go-plugin" "github.com/hashicorp/hcl" + "github.com/hashicorp/terraform/command" tfplugin "github.com/hashicorp/terraform/plugin" "github.com/hashicorp/terraform/terraform" "github.com/kardianos/osext" @@ -74,18 +76,22 @@ func LoadConfig(path string) (*Config, error) { return &result, nil } -// Discover discovers plugins. +// Discover plugins located on disk, and fall back on plugins baked into the +// Terraform binary. // -// This looks in the directory of the executable and the CWD, in that -// order for priority. +// We look in the following places for plugins: +// +// 1. Terraform configuration path +// 2. Path where Terraform is installed +// 3. Path where Terraform is invoked +// +// Whichever file is discoverd LAST wins. +// +// Finally, we look at the list of plugins compiled into Terraform. If any of +// them has not been found on disk we use the internal version. This allows +// users to add / replace plugins without recompiling the main binary. func (c *Config) Discover() error { - // Look in the cwd. - if err := c.discover("."); err != nil { - return err - } - - // Look in the plugins directory. This will override any found - // in the current directory. + // Look in ~/.terraform.d/plugins/ dir, err := ConfigDir() if err != nil { log.Printf("[ERR] Error loading config directory: %s", err) @@ -95,8 +101,8 @@ func (c *Config) Discover() error { } } - // Next, look in the same directory as the executable. Any conflicts - // will overwrite those found in our current directory. + // Next, look in the same directory as the Terraform executable, usually + // /usr/local/bin. If found, this replaces what we found in the config path. exePath, err := osext.Executable() if err != nil { log.Printf("[ERR] Error loading exe directory: %s", err) @@ -106,6 +112,33 @@ func (c *Config) Discover() error { } } + // Finally look in the cwd (where we are invoke Terraform). If found, this + // replaces anything we found in the config / install paths. + if err := c.discover("."); err != nil { + return err + } + + // Finally, if we have a plugin compiled into Terraform and we didn't find + // a replacement on disk, we'll just use the internal version. + for name, _ := range command.InternalProviders { + if _, found := c.Providers[name]; !found { + cmd, err := command.BuildPluginCommandString("provider", name) + if err != nil { + return err + } + c.Providers[name] = cmd + } + } + for name, _ := range command.InternalProvisioners { + if _, found := c.Provisioners[name]; !found { + cmd, err := command.BuildPluginCommandString("provisioner", name) + if err != nil { + return err + } + c.Provisioners[name] = cmd + } + } + return nil } @@ -285,6 +318,12 @@ func pluginCmd(path string) *exec.Cmd { } } + // No plugin binary found, so try to use an internal plugin. + if strings.Contains(path, command.TFSPACE) { + parts := strings.Split(path, command.TFSPACE) + return exec.Command(parts[0], parts[1:]...) + } + // If we still don't have a path, then just set it to the original // given path. if cmdPath == "" { diff --git a/help.go b/help.go index b621f710b..fcc6dc95d 100644 --- a/help.go +++ b/help.go @@ -52,6 +52,11 @@ func listCommands(commands map[string]cli.CommandFactory, maxKeyLen int) string // key length so they can be aligned properly. keys := make([]string, 0, len(commands)) for key, _ := range commands { + // This is an internal command that users should never call directly so + // we will hide it from the command listing. + if key == "internal-plugin" { + continue + } keys = append(keys, key) } sort.Strings(keys) diff --git a/scripts/build.sh b/scripts/build.sh index 76ff6dad6..b7d6856f1 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -47,16 +47,8 @@ gox \ -os="${XC_OS}" \ -arch="${XC_ARCH}" \ -ldflags "${LD_FLAGS}" \ - -output "pkg/{{.OS}}_{{.Arch}}/terraform-{{.Dir}}" \ - $(go list ./... | grep -v /vendor/) - -# Make sure "terraform-terraform" is renamed properly -for PLATFORM in $(find ./pkg -mindepth 1 -maxdepth 1 -type d); do - set +e - mv ${PLATFORM}/terraform-terraform.exe ${PLATFORM}/terraform.exe 2>/dev/null - mv ${PLATFORM}/terraform-terraform ${PLATFORM}/terraform 2>/dev/null - set -e -done + -output "pkg/{{.OS}}_{{.Arch}}/terraform" \ + . # Move all the compiled things to the $GOPATH/bin GOPATH=${GOPATH:-$(go env GOPATH)} diff --git a/scripts/generate-plugins.go b/scripts/generate-plugins.go new file mode 100644 index 000000000..0867f9755 --- /dev/null +++ b/scripts/generate-plugins.go @@ -0,0 +1,283 @@ +// Generate Plugins is a small program that updates the lists of plugins in +// command/internal_plugin_list.go so they will be compiled into the main +// terraform binary. +package main + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "io/ioutil" + "log" + "os" + "path/filepath" + "sort" + "strings" +) + +const target = "command/internal_plugin_list.go" + +func main() { + wd, _ := os.Getwd() + if filepath.Base(wd) != "terraform" { + log.Fatalf("This program must be invoked in the terraform project root; in %s", wd) + } + + // Collect all of the data we need about plugins we have in the project + providers, err := discoverProviders() + if err != nil { + log.Fatalf("Failed to discover providers: %s", err) + } + + provisioners, err := discoverProvisioners() + if err != nil { + log.Fatalf("Failed to discover provisioners: %s", err) + } + + // Do some simple code generation and templating + output := source + output = strings.Replace(output, "IMPORTS", makeImports(providers, provisioners), 1) + output = strings.Replace(output, "PROVIDERS", makeProviderMap(providers), 1) + output = strings.Replace(output, "PROVISIONERS", makeProvisionerMap(provisioners), 1) + + // TODO sort the lists of plugins so we are not subjected to random OS ordering of the plugin lists + + // Write our generated code to the command/plugin.go file + file, err := os.Create(target) + defer file.Close() + if err != nil { + log.Fatalf("Failed to open %s for writing: %s", target, err) + } + + _, err = file.WriteString(output) + if err != nil { + log.Fatalf("Failed writing to %s: %s", target, err) + } + + log.Printf("Generated %s", target) +} + +type plugin struct { + Package string // Package name from ast remoteexec + PluginName string // Path via deriveName() remote-exec + TypeName string // Type of plugin provisioner + Path string // Relative import path builtin/provisioners/remote-exec + ImportName string // See deriveImport() remoteexecprovisioner +} + +// makeProviderMap creates a map of providers like this: +// +// var InternalProviders = map[string]plugin.ProviderFunc{ +// "aws": aws.Provider, +// "azurerm": azurerm.Provider, +// "cloudflare": cloudflare.Provider, +func makeProviderMap(items []plugin) string { + output := "" + for _, item := range items { + output += fmt.Sprintf("\t\"%s\": %s.%s,\n", item.PluginName, item.ImportName, item.TypeName) + } + return output +} + +// makeProvisionerMap creates a map of provisioners like this: +// +// "file": func() terraform.ResourceProvisioner { return new(file.ResourceProvisioner) }, +// "local-exec": func() terraform.ResourceProvisioner { return new(localexec.ResourceProvisioner) }, +// "remote-exec": func() terraform.ResourceProvisioner { return new(remoteexec.ResourceProvisioner) }, +// +// This is more verbose than the Provider case because there is no corresponding +// Provisioner function. +func makeProvisionerMap(items []plugin) string { + output := "" + for _, item := range items { + output += fmt.Sprintf("\t\"%s\": func() terraform.ResourceProvisioner { return new(%s.%s) },\n", item.PluginName, item.ImportName, item.TypeName) + } + return output +} + +func makeImports(providers, provisioners []plugin) string { + plugins := []string{} + + for _, provider := range providers { + plugins = append(plugins, fmt.Sprintf("\t%s \"github.com/hashicorp/terraform/%s\"\n", provider.ImportName, filepath.ToSlash(provider.Path))) + } + + for _, provisioner := range provisioners { + plugins = append(plugins, fmt.Sprintf("\t%s \"github.com/hashicorp/terraform/%s\"\n", provisioner.ImportName, filepath.ToSlash(provisioner.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() { + if item.Name() == "test-fixtures" { + 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 relative to the specified root +// path. +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 is important for disambigutating between +// providers and provisioners that have the same name. This will be something +// like: +// +// remote-exec -> remoteexecprovisioner +// +// which is long, but is 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 using go's ast and +// returns a list of plugins it finds. +func discoverTypesInPath(path, typeID, typeName string) ([]plugin, error) { + pluginTypes := []plugin{} + + dirs, err := listDirectories(path) + if err != nil { + return pluginTypes, err + } + + for _, dir := range dirs { + fset := token.NewFileSet() + goPackages, err := parser.ParseDir(fset, dir, nil, parser.AllErrors) + if err != nil { + return pluginTypes, 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.FuncDecl: + // If we get a function then we will check the function name + // against typeName and the function return type (Results) + // against typeID. + // + // There may be more than one return type but in the target + // case there should only be one. Also the return type is a + // ast.SelectorExpr which means we have multiple nodes. + // We'll read all of them as ast.Ident (identifier), join + // them via . to get a string like terraform.ResourceProvider + // and see if it matches our expected typeID + // + // This is somewhat verbose but prevents us from identifying + // the wrong types if the function name is amiguous or if + // there are other subfolders added later. + if x.Name.Name == typeName && len(x.Type.Results.List) == 1 { + node := x.Type.Results.List[0].Type + typeIdentifiers := []string{} + ast.Inspect(node, func(m ast.Node) bool { + switch y := m.(type) { + case *ast.Ident: + typeIdentifiers = append(typeIdentifiers, y.Name) + } + // We need all of the identifiers to join so we + // can't break early here. + return true + }) + if strings.Join(typeIdentifiers, ".") == typeID { + derivedName := deriveName(path, dir) + pluginTypes = append(pluginTypes, plugin{ + Package: goPackage.Name, + PluginName: derivedName, + ImportName: deriveImport(x.Name.Name, derivedName), + TypeName: x.Name.Name, + Path: dir, + }) + } + } + case *ast.TypeSpec: + // In the simpler case we will simply check whether the type + // declaration has the name we were looking for. + if x.Name.Name == typeID { + derivedName := deriveName(path, dir) + pluginTypes = append(pluginTypes, 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. + return false + } + } + return true + }) + } + } + + return pluginTypes, nil +} + +func discoverProviders() ([]plugin, error) { + path := "./builtin/providers" + typeID := "terraform.ResourceProvider" + typeName := "Provider" + return discoverTypesInPath(path, typeID, typeName) +} + +func discoverProvisioners() ([]plugin, error) { + path := "./builtin/provisioners" + typeID := "ResourceProvisioner" + typeName := "" + return discoverTypesInPath(path, typeID, typeName) +} + +const source = `// +build !core + +// +// This file is automatically generated by scripts/generate-plugins.go -- Do not edit! +// +package command + +import ( +IMPORTS + "github.com/hashicorp/terraform/plugin" + "github.com/hashicorp/terraform/terraform" +) + +var InternalProviders = map[string]plugin.ProviderFunc{ +PROVIDERS +} + +var InternalProvisioners = map[string]plugin.ProvisionerFunc{ +PROVISIONERS +} + +` diff --git a/scripts/generate-plugins_test.go b/scripts/generate-plugins_test.go new file mode 100644 index 000000000..bbb3fce18 --- /dev/null +++ b/scripts/generate-plugins_test.go @@ -0,0 +1,102 @@ +package main + +import "testing" + +func TestMakeProvisionerMap(t *testing.T) { + p := makeProvisionerMap([]plugin{ + { + Package: "file", + PluginName: "file", + TypeName: "ResourceProvisioner", + Path: "builtin/provisioners/file", + ImportName: "fileresourceprovisioner", + }, + { + Package: "localexec", + PluginName: "local-exec", + TypeName: "ResourceProvisioner", + Path: "builtin/provisioners/local-exec", + ImportName: "localexecresourceprovisioner", + }, + { + Package: "remoteexec", + PluginName: "remote-exec", + TypeName: "ResourceProvisioner", + Path: "builtin/provisioners/remote-exec", + ImportName: "remoteexecresourceprovisioner", + }, + }) + + expected := ` "file": func() terraform.ResourceProvisioner { return new(fileresourceprovisioner.ResourceProvisioner) }, + "local-exec": func() terraform.ResourceProvisioner { return new(localexecresourceprovisioner.ResourceProvisioner) }, + "remote-exec": func() terraform.ResourceProvisioner { return new(remoteexecresourceprovisioner.ResourceProvisioner) }, +` + + if p != expected { + t.Errorf("Provisioner output does not match expected format.\n -- Expected -- \n%s\n -- Found --\n%s\n", expected, p) + } +} + +func TestDeriveName(t *testing.T) { + actual := deriveName("builtin/provisioners", "builtin/provisioners/magic/remote-exec") + expected := "magic-remote-exec" + if actual != expected { + t.Errorf("Expected %s; found %s", expected, actual) + } +} + +func TestDeriveImport(t *testing.T) { + actual := deriveImport("provider", "magic-aws") + expected := "magicawsprovider" + if actual != expected { + t.Errorf("Expected %s; found %s", expected, actual) + } +} + +func contains(plugins []plugin, name string) bool { + for _, plugin := range plugins { + if plugin.PluginName == name { + return true + } + } + return false +} + +func TestDiscoverTypesProviders(t *testing.T) { + plugins, err := discoverTypesInPath("../builtin/providers", "terraform.ResourceProvider", "Provider") + if err != nil { + t.Fatalf(err.Error()) + } + // We're just going to spot-check, not do this exhaustively + if !contains(plugins, "aws") { + t.Errorf("Expected to find aws provider") + } + if !contains(plugins, "docker") { + t.Errorf("Expected to find docker provider") + } + if !contains(plugins, "dnsimple") { + t.Errorf("Expected to find dnsimple provider") + } + if !contains(plugins, "triton") { + t.Errorf("Expected to find triton provider") + } + if contains(plugins, "file") { + t.Errorf("Found unexpected provider file") + } +} + +func TestDiscoverTypesProvisioners(t *testing.T) { + plugins, err := discoverTypesInPath("../builtin/provisioners", "ResourceProvisioner", "") + if err != nil { + t.Fatalf(err.Error()) + } + if !contains(plugins, "chef") { + t.Errorf("Expected to find chef provisioner") + } + if !contains(plugins, "remote-exec") { + t.Errorf("Expected to find remote-exec provisioner") + } + if contains(plugins, "aws") { + t.Errorf("Found unexpected provisioner aws") + } +} diff --git a/website/source/downloads.html.erb b/website/source/downloads.html.erb index bf89b1441..87179f12e 100644 --- a/website/source/downloads.html.erb +++ b/website/source/downloads.html.erb @@ -31,6 +31,10 @@ description: |-

Checkout the v<%= latest_version %> CHANGELOG for information on the latest release.

+

+ Note: Terraform now ships as a single binary. When upgrading from Terraform < 0.7.0 + you will need to remove the old terraform-* plugins from your installation path. +