Implemented internal plugin calls; which allows us to compile plugins into the main terraform binary
This commit is contained in:
parent
35c87836b4
commit
6360e6c8b6
4
Makefile
4
Makefile
|
@ -20,10 +20,11 @@ quickdev: generate
|
||||||
# target should be used.
|
# target should be used.
|
||||||
core-dev: generate
|
core-dev: generate
|
||||||
go install github.com/hashicorp/terraform
|
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")
|
# Shorthand for quickly testing the core of Terraform (i.e. "not providers")
|
||||||
core-test: generate
|
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.
|
# Shorthand for building and installing just one plugin for local testing.
|
||||||
# Run as (for example): make plugin-dev PLUGIN=provider-aws
|
# Run as (for example): make plugin-dev PLUGIN=provider-aws
|
||||||
|
@ -77,6 +78,7 @@ generate:
|
||||||
go get -u golang.org/x/tools/cmd/stringer; \
|
go get -u golang.org/x/tools/cmd/stringer; \
|
||||||
fi
|
fi
|
||||||
go generate $$(go list ./... | grep -v /vendor/)
|
go generate $$(go list ./... | grep -v /vendor/)
|
||||||
|
@go fmt command/internal_plugin_list.go > /dev/null
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
gofmt -w .
|
gofmt -w .
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
|
@ -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{}
|
|
@ -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) },
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -79,6 +79,12 @@ func init() {
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"internal-plugin": func() (cli.Command, error) {
|
||||||
|
return &command.InternalPluginCommand{
|
||||||
|
Meta: meta,
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
|
||||||
"output": func() (cli.Command, error) {
|
"output": func() (cli.Command, error) {
|
||||||
return &command.OutputCommand{
|
return &command.OutputCommand{
|
||||||
Meta: meta,
|
Meta: meta,
|
||||||
|
|
63
config.go
63
config.go
|
@ -1,3 +1,4 @@
|
||||||
|
//go:generate go run ./scripts/generate-plugins.go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -11,6 +12,7 @@ import (
|
||||||
|
|
||||||
"github.com/hashicorp/go-plugin"
|
"github.com/hashicorp/go-plugin"
|
||||||
"github.com/hashicorp/hcl"
|
"github.com/hashicorp/hcl"
|
||||||
|
"github.com/hashicorp/terraform/command"
|
||||||
tfplugin "github.com/hashicorp/terraform/plugin"
|
tfplugin "github.com/hashicorp/terraform/plugin"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
"github.com/kardianos/osext"
|
"github.com/kardianos/osext"
|
||||||
|
@ -74,18 +76,22 @@ func LoadConfig(path string) (*Config, error) {
|
||||||
return &result, nil
|
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
|
// We look in the following places for plugins:
|
||||||
// order for priority.
|
//
|
||||||
|
// 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 {
|
func (c *Config) Discover() error {
|
||||||
// Look in the cwd.
|
// Look in ~/.terraform.d/plugins/
|
||||||
if err := c.discover("."); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look in the plugins directory. This will override any found
|
|
||||||
// in the current directory.
|
|
||||||
dir, err := ConfigDir()
|
dir, err := ConfigDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERR] Error loading config directory: %s", err)
|
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
|
// Next, look in the same directory as the Terraform executable, usually
|
||||||
// will overwrite those found in our current directory.
|
// /usr/local/bin. If found, this replaces what we found in the config path.
|
||||||
exePath, err := osext.Executable()
|
exePath, err := osext.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERR] Error loading exe directory: %s", err)
|
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
|
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
|
// If we still don't have a path, then just set it to the original
|
||||||
// given path.
|
// given path.
|
||||||
if cmdPath == "" {
|
if cmdPath == "" {
|
||||||
|
|
5
help.go
5
help.go
|
@ -52,6 +52,11 @@ func listCommands(commands map[string]cli.CommandFactory, maxKeyLen int) string
|
||||||
// key length so they can be aligned properly.
|
// key length so they can be aligned properly.
|
||||||
keys := make([]string, 0, len(commands))
|
keys := make([]string, 0, len(commands))
|
||||||
for key, _ := range 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)
|
keys = append(keys, key)
|
||||||
}
|
}
|
||||||
sort.Strings(keys)
|
sort.Strings(keys)
|
||||||
|
|
|
@ -47,16 +47,8 @@ gox \
|
||||||
-os="${XC_OS}" \
|
-os="${XC_OS}" \
|
||||||
-arch="${XC_ARCH}" \
|
-arch="${XC_ARCH}" \
|
||||||
-ldflags "${LD_FLAGS}" \
|
-ldflags "${LD_FLAGS}" \
|
||||||
-output "pkg/{{.OS}}_{{.Arch}}/terraform-{{.Dir}}" \
|
-output "pkg/{{.OS}}_{{.Arch}}/terraform" \
|
||||||
$(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
|
|
||||||
|
|
||||||
# Move all the compiled things to the $GOPATH/bin
|
# Move all the compiled things to the $GOPATH/bin
|
||||||
GOPATH=${GOPATH:-$(go env GOPATH)}
|
GOPATH=${GOPATH:-$(go env GOPATH)}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
`
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,6 +31,10 @@ description: |-
|
||||||
<p>
|
<p>
|
||||||
Checkout the <a href="https://github.com/hashicorp/terraform/blob/v<%= latest_version %>/CHANGELOG.md">v<%= latest_version %> CHANGELOG</a> for information on the latest release.
|
Checkout the <a href="https://github.com/hashicorp/terraform/blob/v<%= latest_version %>/CHANGELOG.md">v<%= latest_version %> CHANGELOG</a> for information on the latest release.
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
<b>Note:</b> Terraform now ships as a single binary. When upgrading from Terraform < 0.7.0
|
||||||
|
you will need to remove the old <code>terraform-*</code> plugins from your installation path.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue