Merge pull request #27077 from hashicorp/jbardin/internal-provisioners

Internal, in-process provisioners
This commit is contained in:
James Bardin 2020-12-02 13:23:13 -05:00 committed by GitHub
commit 0c107e502f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 1851 additions and 3895 deletions

View File

@ -6,12 +6,6 @@ VERSION?="0.3.44"
# "make protobuf".
generate:
go generate ./...
# go fmt doesn't support -mod=vendor but it still wants to populate the
# module cache with everything in go.mod even though formatting requires
# no dependencies, and so we're disabling modules mode for this right
# now until the "go fmt" behavior is rationalized to either support the
# -mod= argument or _not_ try to install things.
GO111MODULE=off go fmt command/internal_plugin_list.go > /dev/null
# We separate the protobuf generation because most development tasks on
# Terraform do not involve changing protobuf files and protoc is not a

View File

@ -1,12 +0,0 @@
package main
import (
"github.com/hashicorp/terraform/builtin/provisioners/file"
"github.com/hashicorp/terraform/plugin"
)
func main() {
plugin.Serve(&plugin.ServeOpts{
ProvisionerFunc: file.Provisioner,
})
}

View File

@ -1,12 +0,0 @@
package main
import (
"github.com/hashicorp/terraform/builtin/provisioners/local-exec"
"github.com/hashicorp/terraform/plugin"
)
func main() {
plugin.Serve(&plugin.ServeOpts{
ProvisionerFunc: localexec.Provisioner,
})
}

View File

@ -1,12 +0,0 @@
package main
import (
"github.com/hashicorp/terraform/builtin/provisioners/remote-exec"
"github.com/hashicorp/terraform/plugin"
)
func main() {
plugin.Serve(&plugin.ServeOpts{
ProvisionerFunc: remoteexec.Provisioner,
})
}

View File

@ -2,96 +2,131 @@ package file
import (
"context"
"errors"
"fmt"
"io/ioutil"
"os"
"sync"
"github.com/hashicorp/terraform/communicator"
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
"github.com/hashicorp/terraform/internal/legacy/terraform"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/provisioners"
"github.com/mitchellh/go-homedir"
"github.com/zclconf/go-cty/cty"
)
func Provisioner() terraform.ResourceProvisioner {
return &schema.Provisioner{
Schema: map[string]*schema.Schema{
"source": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"content"},
func New() provisioners.Interface {
return &provisioner{}
}
type provisioner struct {
// this stored from the running context, so that Stop() can
// cancel the transfer
mu sync.Mutex
cancel context.CancelFunc
}
func (p *provisioner) GetSchema() (resp provisioners.GetSchemaResponse) {
schema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"source": {
Type: cty.String,
Optional: true,
},
"content": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"source"},
"content": {
Type: cty.String,
Optional: true,
},
"destination": &schema.Schema{
Type: schema.TypeString,
"destination": {
Type: cty.String,
Required: true,
},
},
ApplyFunc: applyFn,
ValidateFunc: validateFn,
}
resp.Provisioner = schema
return resp
}
func applyFn(ctx context.Context) error {
connState := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState)
data := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
// Get a new communicator
comm, err := communicator.New(connState)
func (p *provisioner) ValidateProvisionerConfig(req provisioners.ValidateProvisionerConfigRequest) (resp provisioners.ValidateProvisionerConfigResponse) {
cfg, err := p.GetSchema().Provisioner.CoerceValue(req.Config)
if err != nil {
return err
resp.Diagnostics = resp.Diagnostics.Append(err)
}
source := cfg.GetAttr("source")
content := cfg.GetAttr("content")
switch {
case !source.IsNull() && !content.IsNull():
resp.Diagnostics = resp.Diagnostics.Append(errors.New("Cannot set both 'source' and 'content'"))
return resp
case source.IsNull() && content.IsNull():
resp.Diagnostics = resp.Diagnostics.Append(errors.New("Must provide one of 'source' or 'content'"))
return resp
}
return resp
}
func (p *provisioner) ProvisionResource(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
p.mu.Lock()
ctx, cancel := context.WithCancel(context.Background())
p.cancel = cancel
p.mu.Unlock()
comm, err := communicator.New(req.Connection)
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
return resp
}
// Get the source
src, deleteSource, err := getSrc(data)
src, deleteSource, err := getSrc(req.Config)
if err != nil {
return err
resp.Diagnostics = resp.Diagnostics.Append(err)
return resp
}
if deleteSource {
defer os.Remove(src)
}
// Begin the file copy
dst := data.Get("destination").(string)
dst := req.Config.GetAttr("destination").AsString()
if err := copyFiles(ctx, comm, src, dst); err != nil {
return err
}
return nil
}
func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) {
if !c.IsSet("source") && !c.IsSet("content") {
es = append(es, fmt.Errorf("Must provide one of 'source' or 'content'"))
resp.Diagnostics = resp.Diagnostics.Append(err)
return resp
}
return ws, es
return resp
}
// getSrc returns the file to use as source
func getSrc(data *schema.ResourceData) (string, bool, error) {
src := data.Get("source").(string)
if content, ok := data.GetOk("content"); ok {
func getSrc(v cty.Value) (string, bool, error) {
content := v.GetAttr("content")
src := v.GetAttr("source")
switch {
case !content.IsNull():
file, err := ioutil.TempFile("", "tf-file-content")
if err != nil {
return "", true, err
}
if _, err = file.WriteString(content.(string)); err != nil {
if _, err = file.WriteString(content.AsString()); err != nil {
return "", true, err
}
return file.Name(), true, nil
}
expansion, err := homedir.Expand(src)
return expansion, false, err
case !src.IsNull():
expansion, err := homedir.Expand(src.AsString())
return expansion, false, err
default:
panic("source and content cannot both be null")
}
}
// copyFiles is used to copy the files from a source to a destination
@ -138,5 +173,17 @@ func copyFiles(ctx context.Context, comm communicator.Communicator, src, dst str
if err != nil {
return fmt.Errorf("Upload failed: %v", err)
}
return err
}
func (p *provisioner) Stop() error {
p.mu.Lock()
defer p.mu.Unlock()
p.cancel()
return nil
}
func (p *provisioner) Close() error {
return nil
}

View File

@ -3,110 +3,95 @@ package file
import (
"testing"
"github.com/hashicorp/terraform/configs/hcl2shim"
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
"github.com/hashicorp/terraform/internal/legacy/terraform"
"github.com/hashicorp/terraform/provisioners"
"github.com/zclconf/go-cty/cty"
)
func TestResourceProvisioner_impl(t *testing.T) {
var _ terraform.ResourceProvisioner = Provisioner()
}
func TestProvisioner(t *testing.T) {
if err := Provisioner().(*schema.Provisioner).InternalValidate(); err != nil {
t.Fatalf("err: %s", err)
}
}
func TestResourceProvider_Validate_good_source(t *testing.T) {
c := testConfig(t, map[string]interface{}{
"source": "/tmp/foo",
"destination": "/tmp/bar",
v := cty.ObjectVal(map[string]cty.Value{
"source": cty.StringVal("/tmp/foo"),
"destination": cty.StringVal("/tmp/bar"),
})
warn, errs := Provisioner().Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
}
if len(errs) > 0 {
t.Fatalf("Errors: %v", errs)
resp := New().ValidateProvisionerConfig(provisioners.ValidateProvisionerConfigRequest{
Config: v,
})
if len(resp.Diagnostics) > 0 {
t.Fatal(resp.Diagnostics.ErrWithWarnings())
}
}
func TestResourceProvider_Validate_good_content(t *testing.T) {
c := testConfig(t, map[string]interface{}{
"content": "value to copy",
"destination": "/tmp/bar",
v := cty.ObjectVal(map[string]cty.Value{
"content": cty.StringVal("value to copy"),
"destination": cty.StringVal("/tmp/bar"),
})
warn, errs := Provisioner().Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
}
if len(errs) > 0 {
t.Fatalf("Errors: %v", errs)
resp := New().ValidateProvisionerConfig(provisioners.ValidateProvisionerConfigRequest{
Config: v,
})
if len(resp.Diagnostics) > 0 {
t.Fatal(resp.Diagnostics.ErrWithWarnings())
}
}
func TestResourceProvider_Validate_good_unknown_variable_value(t *testing.T) {
c := testConfig(t, map[string]interface{}{
"content": hcl2shim.UnknownVariableValue,
"destination": "/tmp/bar",
v := cty.ObjectVal(map[string]cty.Value{
"content": cty.UnknownVal(cty.String),
"destination": cty.StringVal("/tmp/bar"),
})
warn, errs := Provisioner().Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
}
if len(errs) > 0 {
t.Fatalf("Errors: %v", errs)
resp := New().ValidateProvisionerConfig(provisioners.ValidateProvisionerConfigRequest{
Config: v,
})
if len(resp.Diagnostics) > 0 {
t.Fatal(resp.Diagnostics.ErrWithWarnings())
}
}
func TestResourceProvider_Validate_bad_not_destination(t *testing.T) {
c := testConfig(t, map[string]interface{}{
"source": "nope",
v := cty.ObjectVal(map[string]cty.Value{
"source": cty.StringVal("nope"),
})
warn, errs := Provisioner().Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
}
if len(errs) == 0 {
t.Fatalf("Should have errors")
resp := New().ValidateProvisionerConfig(provisioners.ValidateProvisionerConfigRequest{
Config: v,
})
if !resp.Diagnostics.HasErrors() {
t.Fatal("Should have errors")
}
}
func TestResourceProvider_Validate_bad_no_source(t *testing.T) {
c := testConfig(t, map[string]interface{}{
"destination": "/tmp/bar",
v := cty.ObjectVal(map[string]cty.Value{
"destination": cty.StringVal("/tmp/bar"),
})
warn, errs := Provisioner().Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
}
if len(errs) == 0 {
t.Fatalf("Should have errors")
resp := New().ValidateProvisionerConfig(provisioners.ValidateProvisionerConfigRequest{
Config: v,
})
if !resp.Diagnostics.HasErrors() {
t.Fatal("Should have errors")
}
}
func TestResourceProvider_Validate_bad_to_many_src(t *testing.T) {
c := testConfig(t, map[string]interface{}{
"source": "nope",
"content": "value to copy",
"destination": "/tmp/bar",
v := cty.ObjectVal(map[string]cty.Value{
"source": cty.StringVal("nope"),
"content": cty.StringVal("vlue to copy"),
"destination": cty.StringVal("/tmp/bar"),
})
warn, errs := Provisioner().Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
}
if len(errs) == 0 {
t.Fatalf("Should have errors")
}
}
resp := New().ValidateProvisionerConfig(provisioners.ValidateProvisionerConfigRequest{
Config: v,
})
func testConfig(t *testing.T, c map[string]interface{}) *terraform.ResourceConfig {
return terraform.NewResourceConfigRaw(c)
if !resp.Diagnostics.HasErrors() {
t.Fatal("Should have errors")
}
}

View File

@ -7,11 +7,13 @@ import (
"os"
"os/exec"
"runtime"
"sync"
"github.com/armon/circbuf"
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
"github.com/hashicorp/terraform/internal/legacy/terraform"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/provisioners"
"github.com/mitchellh/go-linereader"
"github.com/zclconf/go-cty/cty"
)
const (
@ -21,59 +23,79 @@ const (
maxBufSize = 8 * 1024
)
func Provisioner() terraform.ResourceProvisioner {
return &schema.Provisioner{
Schema: map[string]*schema.Schema{
"command": &schema.Schema{
Type: schema.TypeString,
func New() provisioners.Interface {
return &provisioner{}
}
type provisioner struct {
// this stored from the running context, so that Stop() can cancel the
// command
mu sync.Mutex
cancel context.CancelFunc
}
func (p *provisioner) GetSchema() (resp provisioners.GetSchemaResponse) {
schema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"command": {
Type: cty.String,
Required: true,
},
"interpreter": &schema.Schema{
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
"interpreter": {
Type: cty.List(cty.String),
Optional: true,
},
"working_dir": &schema.Schema{
Type: schema.TypeString,
"working_dir": {
Type: cty.String,
Optional: true,
},
"environment": &schema.Schema{
Type: schema.TypeMap,
"environment": {
Type: cty.Map(cty.String),
Optional: true,
},
},
ApplyFunc: applyFn,
}
resp.Provisioner = schema
return resp
}
func applyFn(ctx context.Context) error {
data := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput)
func (p *provisioner) ValidateProvisionerConfig(req provisioners.ValidateProvisionerConfigRequest) (resp provisioners.ValidateProvisionerConfigResponse) {
if _, err := p.GetSchema().Provisioner.CoerceValue(req.Config); err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
}
return resp
}
command := data.Get("command").(string)
func (p *provisioner) ProvisionResource(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
p.mu.Lock()
ctx, cancel := context.WithCancel(context.Background())
p.cancel = cancel
p.mu.Unlock()
command := req.Config.GetAttr("command").AsString()
if command == "" {
return fmt.Errorf("local-exec provisioner command must be a non-empty string")
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("local-exec provisioner command must be a non-empty string"))
return resp
}
// Execute the command with env
environment := data.Get("environment").(map[string]interface{})
envVal := req.Config.GetAttr("environment")
var env []string
for k := range environment {
entry := fmt.Sprintf("%s=%s", k, environment[k].(string))
env = append(env, entry)
if !envVal.IsNull() {
for k, v := range envVal.AsValueMap() {
entry := fmt.Sprintf("%s=%s", k, v.AsString())
env = append(env, entry)
}
}
// Execute the command using a shell
interpreter := data.Get("interpreter").([]interface{})
intrVal := req.Config.GetAttr("interpreter")
var cmdargs []string
if len(interpreter) > 0 {
for _, i := range interpreter {
if arg, ok := i.(string); ok {
cmdargs = append(cmdargs, arg)
}
if !intrVal.IsNull() && intrVal.LengthInt() > 0 {
for _, v := range intrVal.AsValueSlice() {
cmdargs = append(cmdargs, v.AsString())
}
} else {
if runtime.GOOS == "windows" {
@ -82,9 +104,13 @@ func applyFn(ctx context.Context) error {
cmdargs = []string{"/bin/sh", "-c"}
}
}
cmdargs = append(cmdargs, command)
workingdir := data.Get("working_dir").(string)
workingdir := ""
if wdVal := req.Config.GetAttr("working_dir"); !wdVal.IsNull() {
workingdir = wdVal.AsString()
}
// Setup the reader that will read the output from the command.
// We use an os.Pipe so that the *os.File can be passed directly to the
@ -92,7 +118,8 @@ func applyFn(ctx context.Context) error {
// See golang.org/issue/18874
pr, pw, err := os.Pipe()
if err != nil {
return fmt.Errorf("failed to initialize pipe for output: %s", err)
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("failed to initialize pipe for output: %s", err))
return resp
}
var cmdEnv []string
@ -118,10 +145,10 @@ func applyFn(ctx context.Context) error {
// copy the teed output to the UI output
copyDoneCh := make(chan struct{})
go copyOutput(o, tee, copyDoneCh)
go copyUIOutput(req.UIOutput, tee, copyDoneCh)
// Output what we're about to run
o.Output(fmt.Sprintf("Executing: %q", cmdargs))
req.UIOutput.Output(fmt.Sprintf("Executing: %q", cmdargs))
// Start the command
err = cmd.Start()
@ -142,14 +169,26 @@ func applyFn(ctx context.Context) error {
}
if err != nil {
return fmt.Errorf("Error running command '%s': %v. Output: %s",
command, err, output.Bytes())
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("Error running command '%s': %v. Output: %s",
command, err, output.Bytes()))
return resp
}
return resp
}
func (p *provisioner) Stop() error {
p.mu.Lock()
defer p.mu.Unlock()
p.cancel()
return nil
}
func copyOutput(o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) {
func (p *provisioner) Close() error {
return nil
}
func copyUIOutput(o provisioners.UIOutput, r io.Reader, doneCh chan<- struct{}) {
defer close(doneCh)
lr := linereader.New(r)
for line := range lr.Ch {

View File

@ -7,31 +7,30 @@ import (
"testing"
"time"
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
"github.com/hashicorp/terraform/internal/legacy/terraform"
"github.com/hashicorp/terraform/provisioners"
"github.com/mitchellh/cli"
"github.com/zclconf/go-cty/cty"
)
func TestResourceProvisioner_impl(t *testing.T) {
var _ terraform.ResourceProvisioner = Provisioner()
}
func TestProvisioner(t *testing.T) {
if err := Provisioner().(*schema.Provisioner).InternalValidate(); err != nil {
t.Fatalf("err: %s", err)
}
}
func TestResourceProvider_Apply(t *testing.T) {
defer os.Remove("test_out")
c := testConfig(t, map[string]interface{}{
"command": "echo foo > test_out",
output := cli.NewMockUi()
p := New()
schema := p.GetSchema().Provisioner
c, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{
"command": cty.StringVal("echo foo > test_out"),
}))
if err != nil {
t.Fatal(err)
}
resp := p.ProvisionResource(provisioners.ProvisionResourceRequest{
Config: c,
UIOutput: output,
})
output := new(terraform.MockUIOutput)
p := Provisioner()
if err := p.Apply(output, nil, c); err != nil {
t.Fatalf("err: %v", err)
if resp.Diagnostics.HasErrors() {
t.Fatalf("err: %v", resp.Diagnostics.Err())
}
// Check the file
@ -48,14 +47,18 @@ func TestResourceProvider_Apply(t *testing.T) {
}
func TestResourceProvider_stop(t *testing.T) {
c := testConfig(t, map[string]interface{}{
output := cli.NewMockUi()
p := New()
schema := p.GetSchema().Provisioner
c, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{
// bash/zsh/ksh will exec a single command in the same process. This
// makes certain there's a subprocess in the shell.
"command": "sleep 30; sleep 30",
})
output := new(terraform.MockUIOutput)
p := Provisioner()
"command": cty.StringVal("sleep 30; sleep 30"),
}))
if err != nil {
t.Fatal(err)
}
doneCh := make(chan struct{})
startTime := time.Now()
@ -65,7 +68,10 @@ func TestResourceProvider_stop(t *testing.T) {
// Because p.Apply is called in a goroutine, trying to t.Fatal() on its
// result would be ignored or would cause a panic if the parent goroutine
// has already completed.
_ = p.Apply(output, nil, c)
_ = p.ProvisionResource(provisioners.ProvisionResourceRequest{
Config: c,
UIOutput: output,
})
}()
mustExceed := (50 * time.Millisecond)
@ -90,51 +96,32 @@ func TestResourceProvider_stop(t *testing.T) {
}
}
func TestResourceProvider_Validate_good(t *testing.T) {
c := testConfig(t, map[string]interface{}{
"command": "echo foo",
})
warn, errs := Provisioner().Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
}
if len(errs) > 0 {
t.Fatalf("Errors: %v", errs)
}
}
func TestResourceProvider_Validate_missing(t *testing.T) {
c := testConfig(t, map[string]interface{}{})
warn, errs := Provisioner().Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
}
if len(errs) == 0 {
t.Fatalf("Should have errors")
}
}
func testConfig(t *testing.T, c map[string]interface{}) *terraform.ResourceConfig {
return terraform.NewResourceConfigRaw(c)
}
func TestResourceProvider_ApplyCustomInterpreter(t *testing.T) {
c := testConfig(t, map[string]interface{}{
"interpreter": []interface{}{"echo", "is"},
"command": "not really an interpreter",
})
output := cli.NewMockUi()
p := New()
output := new(terraform.MockUIOutput)
p := Provisioner()
schema := p.GetSchema().Provisioner
if err := p.Apply(output, nil, c); err != nil {
t.Fatalf("err: %v", err)
c, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{
"interpreter": cty.ListVal([]cty.Value{cty.StringVal("echo"), cty.StringVal("is")}),
"command": cty.StringVal("not really an interpreter"),
}))
if err != nil {
t.Fatal(err)
}
got := strings.TrimSpace(output.OutputMessage)
want := "is not really an interpreter"
resp := p.ProvisionResource(provisioners.ProvisionResourceRequest{
Config: c,
UIOutput: output,
})
if resp.Diagnostics.HasErrors() {
t.Fatal(resp.Diagnostics.Err())
}
got := strings.TrimSpace(output.OutputWriter.String())
want := `Executing: ["echo" "is" "not really an interpreter"]
is not really an interpreter`
if got != want {
t.Errorf("wrong output\ngot: %s\nwant: %s", got, want)
}
@ -145,16 +132,25 @@ func TestResourceProvider_ApplyCustomWorkingDirectory(t *testing.T) {
os.Mkdir(testdir, 0755)
defer os.Remove(testdir)
c := testConfig(t, map[string]interface{}{
"working_dir": testdir,
"command": "echo `pwd`",
output := cli.NewMockUi()
p := New()
schema := p.GetSchema().Provisioner
c, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{
"working_dir": cty.StringVal(testdir),
"command": cty.StringVal("echo `pwd`"),
}))
if err != nil {
t.Fatal(err)
}
resp := p.ProvisionResource(provisioners.ProvisionResourceRequest{
Config: c,
UIOutput: output,
})
output := new(terraform.MockUIOutput)
p := Provisioner()
if err := p.Apply(output, nil, c); err != nil {
t.Fatalf("err: %v", err)
if resp.Diagnostics.HasErrors() {
t.Fatal(resp.Diagnostics.Err())
}
dir, err := os.Getwd()
@ -162,32 +158,41 @@ func TestResourceProvider_ApplyCustomWorkingDirectory(t *testing.T) {
t.Fatalf("err: %v", err)
}
got := strings.TrimSpace(output.OutputMessage)
want := dir + "/" + testdir
got := strings.TrimSpace(output.OutputWriter.String())
want := "Executing: [\"/bin/sh\" \"-c\" \"echo `pwd`\"]\n" + dir + "/" + testdir
if got != want {
t.Errorf("wrong output\ngot: %s\nwant: %s", got, want)
}
}
func TestResourceProvider_ApplyCustomEnv(t *testing.T) {
c := testConfig(t, map[string]interface{}{
"command": "echo $FOO $BAR $BAZ",
"environment": map[string]interface{}{
"FOO": "BAR",
"BAR": 1,
"BAZ": "true",
},
})
output := cli.NewMockUi()
p := New()
schema := p.GetSchema().Provisioner
output := new(terraform.MockUIOutput)
p := Provisioner()
if err := p.Apply(output, nil, c); err != nil {
t.Fatalf("err: %v", err)
c, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{
"command": cty.StringVal("echo $FOO $BAR $BAZ"),
"environment": cty.MapVal(map[string]cty.Value{
"FOO": cty.StringVal("BAR"),
"BAR": cty.StringVal("1"),
"BAZ": cty.StringVal("true"),
}),
}))
if err != nil {
t.Fatal(err)
}
got := strings.TrimSpace(output.OutputMessage)
want := "BAR 1 true"
resp := p.ProvisionResource(provisioners.ProvisionResourceRequest{
Config: c,
UIOutput: output,
})
if resp.Diagnostics.HasErrors() {
t.Fatal(resp.Diagnostics.Err())
}
got := strings.TrimSpace(output.OutputWriter.String())
want := `Executing: ["/bin/sh" "-c" "echo $FOO $BAR $BAZ"]
BAR 1 true`
if got != want {
t.Errorf("wrong output\ngot: %s\nwant: %s", got, want)
}

View File

@ -3,92 +3,135 @@ package remoteexec
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"strings"
"time"
"sync"
"github.com/hashicorp/terraform/communicator"
"github.com/hashicorp/terraform/communicator/remote"
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
"github.com/hashicorp/terraform/internal/legacy/terraform"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/provisioners"
"github.com/mitchellh/go-linereader"
"github.com/zclconf/go-cty/cty"
)
// maxBackoffDealy is the maximum delay between retry attempts
var maxBackoffDelay = 10 * time.Second
var initialBackoffDelay = time.Second
func Provisioner() terraform.ResourceProvisioner {
return &schema.Provisioner{
Schema: map[string]*schema.Schema{
"inline": {
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
PromoteSingle: true,
Optional: true,
ConflictsWith: []string{"script", "scripts"},
},
"script": {
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"inline", "scripts"},
},
"scripts": {
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
ConflictsWith: []string{"script", "inline"},
},
},
ApplyFunc: applyFn,
}
func New() provisioners.Interface {
return &provisioner{}
}
// Apply executes the remote exec provisioner
func applyFn(ctx context.Context) error {
connState := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState)
data := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput)
type provisioner struct {
// this stored from the running context, so that Stop() can cancel the
// command
mu sync.Mutex
cancel context.CancelFunc
}
// Get a new communicator
comm, err := communicator.New(connState)
func (p *provisioner) GetSchema() (resp provisioners.GetSchemaResponse) {
schema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"inline": {
Type: cty.List(cty.String),
Optional: true,
},
"script": {
Type: cty.String,
Optional: true,
},
"scripts": {
Type: cty.List(cty.String),
Optional: true,
},
},
}
resp.Provisioner = schema
return resp
}
func (p *provisioner) ValidateProvisionerConfig(req provisioners.ValidateProvisionerConfigRequest) (resp provisioners.ValidateProvisionerConfigResponse) {
cfg, err := p.GetSchema().Provisioner.CoerceValue(req.Config)
if err != nil {
return err
resp.Diagnostics = resp.Diagnostics.Append(err)
return resp
}
inline := cfg.GetAttr("inline")
script := cfg.GetAttr("script")
scripts := cfg.GetAttr("scripts")
set := 0
if !inline.IsNull() {
set++
}
if !script.IsNull() {
set++
}
if !scripts.IsNull() {
set++
}
if set != 1 {
resp.Diagnostics = resp.Diagnostics.Append(errors.New(
`only one of "inline", "script", or "scripts" must be set`))
}
return resp
}
func (p *provisioner) ProvisionResource(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
p.mu.Lock()
ctx, cancel := context.WithCancel(context.Background())
p.cancel = cancel
p.mu.Unlock()
comm, err := communicator.New(req.Connection)
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
return resp
}
// Collect the scripts
scripts, err := collectScripts(data)
scripts, err := collectScripts(req.Config)
if err != nil {
return err
resp.Diagnostics = resp.Diagnostics.Append(err)
return resp
}
for _, s := range scripts {
defer s.Close()
}
// Copy and execute each script
if err := runScripts(ctx, o, comm, scripts); err != nil {
return err
if err := runScripts(ctx, req.UIOutput, comm, scripts); err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
return resp
}
return resp
}
func (p *provisioner) Stop() error {
p.mu.Lock()
defer p.mu.Unlock()
p.cancel()
return nil
}
func (p *provisioner) Close() error {
return nil
}
// generateScripts takes the configuration and creates a script from each inline config
func generateScripts(d *schema.ResourceData) ([]string, error) {
func generateScripts(inline cty.Value) ([]string, error) {
var lines []string
for _, l := range d.Get("inline").([]interface{}) {
line, ok := l.(string)
if !ok {
return nil, fmt.Errorf("Error parsing %v as a string", l)
for _, l := range inline.AsValueSlice() {
s := l.AsString()
if s == "" {
return nil, errors.New("invalid empty string in 'scripts'")
}
lines = append(lines, line)
lines = append(lines, s)
}
lines = append(lines, "")
@ -97,10 +140,10 @@ func generateScripts(d *schema.ResourceData) ([]string, error) {
// collectScripts is used to collect all the scripts we need
// to execute in preparation for copying them.
func collectScripts(d *schema.ResourceData) ([]io.ReadCloser, error) {
func collectScripts(v cty.Value) ([]io.ReadCloser, error) {
// Check if inline
if _, ok := d.GetOk("inline"); ok {
scripts, err := generateScripts(d)
if inline := v.GetAttr("inline"); !inline.IsNull() {
scripts, err := generateScripts(inline)
if err != nil {
return nil, err
}
@ -115,21 +158,21 @@ func collectScripts(d *schema.ResourceData) ([]io.ReadCloser, error) {
// Collect scripts
var scripts []string
if script, ok := d.GetOk("script"); ok {
scr, ok := script.(string)
if !ok {
return nil, fmt.Errorf("Error parsing script %v as string", script)
if script := v.GetAttr("script"); !script.IsNull() {
s := script.AsString()
if s == "" {
return nil, errors.New("invalid empty string in 'script'")
}
scripts = append(scripts, scr)
scripts = append(scripts, s)
}
if scriptList, ok := d.GetOk("scripts"); ok {
for _, script := range scriptList.([]interface{}) {
scr, ok := script.(string)
if !ok {
return nil, fmt.Errorf("Error parsing script %v as string", script)
if scriptList := v.GetAttr("scripts"); !scriptList.IsNull() {
for _, script := range scriptList.AsValueSlice() {
s := script.AsString()
if s == "" {
return nil, errors.New("invalid empty string in 'script'")
}
scripts = append(scripts, scr)
scripts = append(scripts, script.AsString())
}
}
@ -151,12 +194,7 @@ func collectScripts(d *schema.ResourceData) ([]io.ReadCloser, error) {
}
// runScripts is used to copy and execute a set of scripts
func runScripts(
ctx context.Context,
o terraform.UIOutput,
comm communicator.Communicator,
scripts []io.ReadCloser) error {
func runScripts(ctx context.Context, o provisioners.UIOutput, comm communicator.Communicator, scripts []io.ReadCloser) error {
retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout())
defer cancel()
@ -182,8 +220,8 @@ func runScripts(
defer outW.Close()
defer errW.Close()
go copyOutput(o, outR)
go copyOutput(o, errR)
go copyUIOutput(o, outR)
go copyUIOutput(o, errR)
remotePath := comm.ScriptPath()
@ -216,8 +254,7 @@ func runScripts(
return nil
}
func copyOutput(
o terraform.UIOutput, r io.Reader) {
func copyUIOutput(o provisioners.UIOutput, r io.Reader) {
lr := linereader.New(r)
for line := range lr.Ch {
o.Output(line)

View File

@ -4,6 +4,7 @@ import (
"bytes"
"context"
"io"
"log"
"testing"
"time"
@ -11,44 +12,34 @@ import (
"github.com/hashicorp/terraform/communicator"
"github.com/hashicorp/terraform/communicator/remote"
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
"github.com/hashicorp/terraform/internal/legacy/terraform"
"github.com/hashicorp/terraform/provisioners"
"github.com/mitchellh/cli"
"github.com/zclconf/go-cty/cty"
)
func TestResourceProvisioner_impl(t *testing.T) {
var _ terraform.ResourceProvisioner = Provisioner()
}
func TestProvisioner(t *testing.T) {
if err := Provisioner().(*schema.Provisioner).InternalValidate(); err != nil {
t.Fatalf("err: %s", err)
}
}
func TestResourceProvider_Validate_good(t *testing.T) {
c := testConfig(t, map[string]interface{}{
"inline": "echo foo",
c := cty.ObjectVal(map[string]cty.Value{
"inline": cty.ListVal([]cty.Value{cty.StringVal("echo foo")}),
})
warn, errs := Provisioner().Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
}
if len(errs) > 0 {
t.Fatalf("Errors: %v", errs)
resp := New().ValidateProvisionerConfig(provisioners.ValidateProvisionerConfigRequest{
Config: c,
})
if len(resp.Diagnostics) > 0 {
t.Fatal(resp.Diagnostics.ErrWithWarnings())
}
}
func TestResourceProvider_Validate_bad(t *testing.T) {
c := testConfig(t, map[string]interface{}{
"invalid": "nope",
c := cty.ObjectVal(map[string]cty.Value{
"invalid": cty.StringVal("nope"),
})
warn, errs := Provisioner().Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
}
if len(errs) == 0 {
resp := New().ValidateProvisionerConfig(provisioners.ValidateProvisionerConfigRequest{
Config: c,
})
if !resp.Diagnostics.HasErrors() {
t.Fatalf("Should have errors")
}
}
@ -59,17 +50,13 @@ exit 0
`
func TestResourceProvider_generateScript(t *testing.T) {
conf := map[string]interface{}{
"inline": []interface{}{
"cd /tmp",
"wget http://foobar",
"exit 0",
},
}
inline := cty.ListVal([]cty.Value{
cty.StringVal("cd /tmp"),
cty.StringVal("wget http://foobar"),
cty.StringVal("exit 0"),
})
out, err := generateScripts(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, conf),
)
out, err := generateScripts(inline)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -84,34 +71,28 @@ func TestResourceProvider_generateScript(t *testing.T) {
}
func TestResourceProvider_generateScriptEmptyInline(t *testing.T) {
p := Provisioner().(*schema.Provisioner)
conf := map[string]interface{}{
"inline": []interface{}{""},
}
inline := cty.ListVal([]cty.Value{cty.StringVal("")})
_, err := generateScripts(schema.TestResourceDataRaw(
t, p.Schema, conf))
_, err := generateScripts(inline)
if err == nil {
t.Fatal("expected error, got none")
}
if !strings.Contains(err.Error(), "Error parsing") {
t.Fatalf("expected parsing error, got: %s", err)
if !strings.Contains(err.Error(), "empty string") {
t.Fatalf("expected empty string error, got: %s", err)
}
}
func TestResourceProvider_CollectScripts_inline(t *testing.T) {
conf := map[string]interface{}{
"inline": []interface{}{
"cd /tmp",
"wget http://foobar",
"exit 0",
},
conf := map[string]cty.Value{
"inline": cty.ListVal([]cty.Value{
cty.StringVal("cd /tmp"),
cty.StringVal("wget http://foobar"),
cty.StringVal("exit 0"),
}),
}
scripts, err := collectScripts(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, conf),
)
scripts, err := collectScripts(cty.ObjectVal(conf))
if err != nil {
t.Fatalf("err: %v", err)
}
@ -132,13 +113,19 @@ func TestResourceProvider_CollectScripts_inline(t *testing.T) {
}
func TestResourceProvider_CollectScripts_script(t *testing.T) {
conf := map[string]interface{}{
"script": "testdata/script1.sh",
p := New()
schema := p.GetSchema().Provisioner
conf, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{
"scripts": cty.ListVal([]cty.Value{
cty.StringVal("testdata/script1.sh"),
}),
}))
if err != nil {
t.Fatal(err)
}
scripts, err := collectScripts(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, conf),
)
scripts, err := collectScripts(conf)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -159,17 +146,21 @@ func TestResourceProvider_CollectScripts_script(t *testing.T) {
}
func TestResourceProvider_CollectScripts_scripts(t *testing.T) {
conf := map[string]interface{}{
"scripts": []interface{}{
"testdata/script1.sh",
"testdata/script1.sh",
"testdata/script1.sh",
},
p := New()
schema := p.GetSchema().Provisioner
conf, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{
"scripts": cty.ListVal([]cty.Value{
cty.StringVal("testdata/script1.sh"),
cty.StringVal("testdata/script1.sh"),
cty.StringVal("testdata/script1.sh"),
}),
}))
if err != nil {
log.Fatal(err)
}
scripts, err := collectScripts(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, conf),
)
scripts, err := collectScripts(conf)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -192,25 +183,28 @@ func TestResourceProvider_CollectScripts_scripts(t *testing.T) {
}
func TestResourceProvider_CollectScripts_scriptsEmpty(t *testing.T) {
p := Provisioner().(*schema.Provisioner)
conf := map[string]interface{}{
"scripts": []interface{}{""},
p := New()
schema := p.GetSchema().Provisioner
conf, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{
"scripts": cty.ListVal([]cty.Value{cty.StringVal("")}),
}))
if err != nil {
t.Fatal(err)
}
_, err := collectScripts(schema.TestResourceDataRaw(
t, p.Schema, conf))
_, err = collectScripts(conf)
if err == nil {
t.Fatal("expected error")
}
if !strings.Contains(err.Error(), "Error parsing") {
t.Fatalf("Expected parsing error, got: %s", err)
if !strings.Contains(err.Error(), "empty string") {
t.Fatalf("Expected empty string error, got: %s", err)
}
}
func TestProvisionerTimeout(t *testing.T) {
o := new(terraform.MockUIOutput)
o := cli.NewMockUi()
c := new(communicator.MockCommunicator)
disconnected := make(chan struct{})
@ -231,13 +225,11 @@ func TestProvisionerTimeout(t *testing.T) {
c.UploadScripts = map[string]string{"hello": "echo hello"}
c.RemoteScriptPath = "hello"
p := Provisioner().(*schema.Provisioner)
conf := map[string]interface{}{
"inline": []interface{}{"echo hello"},
conf := map[string]cty.Value{
"inline": cty.ListVal([]cty.Value{cty.StringVal("echo hello")}),
}
scripts, err := collectScripts(schema.TestResourceDataRaw(
t, p.Schema, conf))
scripts, err := collectScripts(cty.ObjectVal(conf))
if err != nil {
t.Fatal(err)
}

View File

@ -33,7 +33,7 @@ func TestProviderDevOverrides(t *testing.T) {
// such as if it stops being buildable into an independent executable.
providerExeDir := filepath.Join(tf.WorkDir(), "pkgdir")
providerExePrefix := filepath.Join(providerExeDir, "terraform-provider-test_")
providerExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/legacy/builtin/bins/provider-test", providerExePrefix)
providerExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple/main", providerExePrefix)
t.Logf("temporary provider executable is %s", providerExe)
err := ioutil.WriteFile(filepath.Join(tf.WorkDir(), "dev.tfrc"), []byte(fmt.Sprintf(`

View File

@ -0,0 +1,65 @@
package e2etest
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/hashicorp/terraform/e2e"
)
// TestProviderDevOverrides is a test that terraform can execute a 3rd party
// provisioner plugin.
func TestProvisionerPlugin(t *testing.T) {
t.Parallel()
// This test reaches out to releases.hashicorp.com to download the
// template and null providers, so it can only run if network access is
// allowed.
skipIfCannotAccessNetwork(t)
tf := e2e.NewBinary(terraformBin, "testdata/provisioner-plugin")
defer tf.Close()
// In order to do a decent end-to-end test for this case we will need a
// real enough provisioner plugin to try to run and make sure we are able
// to actually run it. Here will build the local-exec provisioner into a
// binary called test-provisioner
provisionerExePrefix := filepath.Join(tf.WorkDir(), "terraform-provisioner-test_")
provisionerExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provisioner-local-exec/main", provisionerExePrefix)
// provisioners must use the old binary name format, so rename this binary
newExe := filepath.Join(tf.WorkDir(), "terraform-provisioner-test")
if _, err := os.Stat(newExe); !os.IsNotExist(err) {
t.Fatalf("%q already exists", newExe)
}
if err := os.Rename(provisionerExe, newExe); err != nil {
t.Fatalf("error renaming provisioner binary: %v", err)
}
provisionerExe = newExe
t.Logf("temporary provisioner executable is %s", provisionerExe)
//// INIT
_, stderr, err := tf.Run("init")
if err != nil {
t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
}
//// PLAN
_, stderr, err = tf.Run("plan", "-out=tfplan")
if err != nil {
t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr)
}
//// APPLY
stdout, stderr, err := tf.Run("apply", "tfplan")
if err != nil {
t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr)
}
if !strings.Contains(stdout, "HelloProvisioner") {
t.Fatalf("missing provisioner output:\n%s", stdout)
}
}

View File

@ -1,14 +1,11 @@
terraform {
required_providers {
test = {
simple = {
source = "example.com/test/test"
version = "2.0.0"
}
}
}
provider "test" {
}
data "test_data_source" "test" {
data "simple_resource" "test" {
}

View File

@ -0,0 +1,5 @@
resource "null_resource" "a" {
provisioner "test" {
command = "echo HelloProvisioner"
}
}

View File

@ -1,6 +1,10 @@
provider "test" {
terraform {
required_providers {
simple = {
source = "hashicorp/test"
}
}
}
resource "test_resource_signal" "test" {
resource "simple_resource" "test" {
}

View File

@ -12,8 +12,8 @@ import (
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform/e2e"
"github.com/hashicorp/terraform/internal/legacy/builtin/providers/test"
grpcplugin "github.com/hashicorp/terraform/internal/legacy/helper/plugin"
"github.com/hashicorp/terraform/internal/grpcwrap"
simple "github.com/hashicorp/terraform/internal/provider-simple"
proto "github.com/hashicorp/terraform/internal/tfplugin5"
tfplugin "github.com/hashicorp/terraform/plugin"
)
@ -42,7 +42,7 @@ type reattachConfigAddr struct {
type providerServer struct {
sync.Mutex
*grpcplugin.GRPCProviderServer
proto.ProviderServer
planResourceChangeCalled bool
applyResourceChangeCalled bool
}
@ -52,7 +52,7 @@ func (p *providerServer) PlanResourceChange(ctx context.Context, req *proto.Plan
defer p.Unlock()
p.planResourceChangeCalled = true
return p.GRPCProviderServer.PlanResourceChange(ctx, req)
return p.ProviderServer.PlanResourceChange(ctx, req)
}
func (p *providerServer) ApplyResourceChange(ctx context.Context, req *proto.ApplyResourceChange_Request) (*proto.ApplyResourceChange_Response, error) {
@ -60,7 +60,7 @@ func (p *providerServer) ApplyResourceChange(ctx context.Context, req *proto.App
defer p.Unlock()
p.applyResourceChangeCalled = true
return p.GRPCProviderServer.ApplyResourceChange(ctx, req)
return p.ProviderServer.ApplyResourceChange(ctx, req)
}
func (p *providerServer) PlanResourceChangeCalled() bool {
@ -99,7 +99,7 @@ func TestUnmanagedSeparatePlan(t *testing.T) {
reattachCh := make(chan *plugin.ReattachConfig)
closeCh := make(chan struct{})
provider := &providerServer{
GRPCProviderServer: grpcplugin.NewGRPCProviderServerShim(test.Provider()),
ProviderServer: grpcwrap.Provider(simple.Provider()),
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@ -140,6 +140,7 @@ func TestUnmanagedSeparatePlan(t *testing.T) {
},
},
})
tf.AddEnv("TF_REATTACH_PROVIDERS=" + string(reattachStr))
tf.AddEnv("PLUGIN_PROTOCOL_VERSION=5")
@ -164,7 +165,7 @@ func TestUnmanagedSeparatePlan(t *testing.T) {
}
if !provider.PlanResourceChangeCalled() {
t.Error("PlanResourceChange not called on in-process provider")
t.Error("PlanResourceChange not called on un-managed provider")
}
//// APPLY
@ -174,7 +175,7 @@ func TestUnmanagedSeparatePlan(t *testing.T) {
}
if !provider.ApplyResourceChangeCalled() {
t.Error("ApplyResourceChange not called on in-process provider")
t.Error("ApplyResourceChange not called on un-managed provider")
}
provider.ResetApplyResourceChangeCalled()

View File

@ -7,7 +7,7 @@ import (
"testing"
"github.com/hashicorp/terraform/e2e"
tfcore "github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/version"
)
func TestVersion(t *testing.T) {
@ -31,7 +31,7 @@ func TestVersion(t *testing.T) {
t.Errorf("unexpected stderr output:\n%s", stderr)
}
wantVersion := fmt.Sprintf("Terraform v%s", tfcore.VersionString())
wantVersion := fmt.Sprintf("Terraform v%s", version.String())
if !strings.Contains(stdout, wantVersion) {
t.Errorf("output does not contain our current version %q:\n%s", wantVersion, stdout)
}
@ -63,7 +63,7 @@ func TestVersionWithProvider(t *testing.T) {
t.Errorf("unexpected stderr output:\n%s", stderr)
}
wantVersion := fmt.Sprintf("Terraform v%s", tfcore.VersionString())
wantVersion := fmt.Sprintf("Terraform v%s", version.String())
if !strings.Contains(stdout, wantVersion) {
t.Errorf("output does not contain our current version %q:\n%s", wantVersion, stdout)
}

View File

@ -1,97 +0,0 @@
package command
import (
"fmt"
"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
}
// Internal plugins do not support any CLI args, but we do receive flags that
// main.go:mergeEnvArgs has merged in from EnvCLI. Instead of making main.go
// aware of this exception, we strip all flags from our args. Flags are easily
// identified by the '-' prefix, ensured by the cli package used.
func StripArgFlags(args []string) []string {
argsNoFlags := []string{}
for i := range args {
if !strings.HasPrefix(args[i], "-") {
argsNoFlags = append(argsNoFlags, args[i])
}
}
return argsNoFlags
}
func (c *InternalPluginCommand) Run(args []string) int {
// strip flags from args, only use subcommands.
args = StripArgFlags(args)
if len(args) != 2 {
log.Printf("Wrong number of args; expected: terraform internal-plugin pluginType pluginName")
return 1
}
pluginType := args[0]
pluginName := args[1]
log.SetPrefix(fmt.Sprintf("%s-%s (internal) ", pluginName, pluginType))
switch pluginType {
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"
}

View File

@ -1,20 +0,0 @@
//
// This file is automatically generated by scripts/generate-plugins.go -- Do not edit!
//
package command
import (
fileprovisioner "github.com/hashicorp/terraform/builtin/provisioners/file"
localexecprovisioner "github.com/hashicorp/terraform/builtin/provisioners/local-exec"
remoteexecprovisioner "github.com/hashicorp/terraform/builtin/provisioners/remote-exec"
"github.com/hashicorp/terraform/plugin"
)
var InternalProviders = map[string]plugin.ProviderFunc{}
var InternalProvisioners = map[string]plugin.ProvisionerFunc{
"file": fileprovisioner.Provisioner,
"local-exec": localexecprovisioner.Provisioner,
"remote-exec": remoteexecprovisioner.Provisioner,
}

View File

@ -1,55 +0,0 @@
package command
import (
"testing"
)
func TestInternalPlugin_InternalProviders(t *testing.T) {
m := new(Meta)
providers := m.internalProviders()
// terraform is the only provider moved back to internal
for _, name := range []string{"terraform"} {
pf, ok := providers[name]
if !ok {
t.Errorf("Expected to find %s in InternalProviders", name)
}
provider, err := pf()
if err != nil {
t.Fatal(err)
}
if provider == nil {
t.Fatal("provider factory returned a nil provider")
}
}
}
func TestInternalPlugin_InternalProvisioners(t *testing.T) {
for _, name := range []string{"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)
}
}
func TestInternalPlugin_StripArgFlags(t *testing.T) {
actual := StripArgFlags([]string{"provisioner", "remote-exec", "-var-file=my_vars.tfvars", "-flag"})
expected := []string{"provisioner", "remote-exec"}
// Must be same length and order.
if len(actual) != len(expected) || expected[0] != actual[0] || actual[1] != actual[1] {
t.Fatalf("Expected args to be exactly '%s', got '%s'", expected, actual)
}
}

View File

@ -9,11 +9,13 @@ import (
"os/exec"
"path/filepath"
"runtime"
"strings"
plugin "github.com/hashicorp/go-plugin"
"github.com/kardianos/osext"
fileprovisioner "github.com/hashicorp/terraform/builtin/provisioners/file"
localexec "github.com/hashicorp/terraform/builtin/provisioners/local-exec"
remoteexec "github.com/hashicorp/terraform/builtin/provisioners/remote-exec"
"github.com/hashicorp/terraform/internal/logging"
tfplugin "github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/plugin/discovery"
@ -134,8 +136,8 @@ func (m *Meta) provisionerFactories() map[string]provisioners.Factory {
// Wire up the internal provisioners first. These might be overridden
// by discovered provisioners below.
for name := range InternalProvisioners {
factories[name] = internalProvisionerFactory(discovery.PluginMeta{Name: name})
for name, factory := range internalProvisionerFactories() {
factories[name] = factory
}
byName := plugins.ByName()
@ -151,29 +153,6 @@ func (m *Meta) provisionerFactories() map[string]provisioners.Factory {
return factories
}
func internalPluginClient(kind, name string) (*plugin.Client, error) {
cmdLine, err := BuildPluginCommandString(kind, name)
if err != nil {
return nil, err
}
// See the docstring for BuildPluginCommandString for why we need to do
// this split here.
cmdArgv := strings.Split(cmdLine, TFSPACE)
cfg := &plugin.ClientConfig{
Cmd: exec.Command(cmdArgv[0], cmdArgv[1:]...),
HandshakeConfig: tfplugin.Handshake,
Managed: true,
VersionedPlugins: tfplugin.VersionedPlugins,
AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
AutoMTLS: enableProviderAutoMTLS,
Logger: logging.NewLogger(kind),
}
return plugin.NewClient(cfg), nil
}
func provisionerFactory(meta discovery.PluginMeta) provisioners.Factory {
return func() (provisioners.Interface, error) {
cfg := &plugin.ClientConfig{
@ -190,13 +169,11 @@ func provisionerFactory(meta discovery.PluginMeta) provisioners.Factory {
}
}
func internalProvisionerFactory(meta discovery.PluginMeta) provisioners.Factory {
return func() (provisioners.Interface, error) {
client, err := internalPluginClient("provisioner", meta.Name)
if err != nil {
return nil, fmt.Errorf("[WARN] failed to build command line for internal plugin %q: %s", meta.Name, err)
}
return newProvisionerClient(client)
func internalProvisionerFactories() map[string]provisioners.Factory {
return map[string]provisioners.Factory{
"file": provisioners.FactoryFixed(fileprovisioner.New()),
"local-exec": provisioners.FactoryFixed(localexec.New()),
"remote-exec": provisioners.FactoryFixed(remoteexec.New()),
}
}

View File

@ -194,12 +194,6 @@ func initCommands(
}, nil
},
"internal-plugin": func() (cli.Command, error) {
return &command.InternalPluginCommand{
Meta: meta,
}, nil
},
"login": func() (cli.Command, error) {
return &command.LoginCommand{
Meta: meta,

View File

@ -9,16 +9,18 @@ import (
"time"
"github.com/hashicorp/terraform/communicator/remote"
"github.com/hashicorp/terraform/communicator/shared"
"github.com/hashicorp/terraform/communicator/ssh"
"github.com/hashicorp/terraform/communicator/winrm"
"github.com/hashicorp/terraform/internal/legacy/terraform"
"github.com/hashicorp/terraform/provisioners"
"github.com/zclconf/go-cty/cty"
)
// Communicator is an interface that must be implemented by all communicators
// used for any of the provisioners
type Communicator interface {
// Connect is used to setup the connection
Connect(terraform.UIOutput) error
Connect(provisioners.UIOutput) error
// Disconnect is used to terminate the connection
Disconnect() error
@ -43,13 +45,23 @@ type Communicator interface {
}
// New returns a configured Communicator or an error if the connection type is not supported
func New(s *terraform.InstanceState) (Communicator, error) {
connType := s.Ephemeral.ConnInfo["type"]
func New(v cty.Value) (Communicator, error) {
v, err := shared.ConnectionBlockSupersetSchema.CoerceValue(v)
if err != nil {
return nil, err
}
typeVal := v.GetAttr("type")
connType := ""
if !typeVal.IsNull() {
connType = typeVal.AsString()
}
switch connType {
case "ssh", "": // The default connection type is ssh, so if connType is empty use ssh
return ssh.New(s)
return ssh.New(v)
case "winrm":
return winrm.New(s)
return winrm.New(v)
default:
return nil, fmt.Errorf("connection type '%s' not supported", connType)
}

View File

@ -8,7 +8,7 @@ import (
"time"
"github.com/hashicorp/terraform/communicator/remote"
"github.com/hashicorp/terraform/internal/legacy/terraform"
"github.com/hashicorp/terraform/provisioners"
)
// MockCommunicator is an implementation of Communicator that can be used for tests.
@ -24,7 +24,7 @@ type MockCommunicator struct {
}
// Connect implementation of communicator.Communicator interface
func (c *MockCommunicator) Connect(o terraform.UIOutput) error {
func (c *MockCommunicator) Connect(o provisioners.UIOutput) error {
return nil
}

View File

@ -8,29 +8,26 @@ import (
"testing"
"time"
"github.com/hashicorp/terraform/internal/legacy/terraform"
"github.com/zclconf/go-cty/cty"
)
func TestCommunicator_new(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "telnet",
"host": "127.0.0.1",
},
},
cfg := map[string]cty.Value{
"type": cty.StringVal("telnet"),
"host": cty.StringVal("127.0.0.1"),
}
if _, err := New(r); err == nil {
if _, err := New(cty.ObjectVal(cfg)); err == nil {
t.Fatalf("expected error with telnet")
}
r.Ephemeral.ConnInfo["type"] = "ssh"
if _, err := New(r); err != nil {
cfg["type"] = cty.StringVal("ssh")
if _, err := New(cty.ObjectVal(cfg)); err != nil {
t.Fatalf("err: %v", err)
}
r.Ephemeral.ConnInfo["type"] = "winrm"
if _, err := New(r); err != nil {
cfg["type"] = cty.StringVal("winrm")
if _, err := New(cty.ObjectVal(cfg)); err != nil {
t.Fatalf("err: %v", err)
}
}

View File

@ -3,8 +3,124 @@ package shared
import (
"fmt"
"net"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/zclconf/go-cty/cty"
)
// ConnectionBlockSupersetSchema is a schema representing the superset of all
// possible arguments for "connection" blocks across all supported connection
// types.
//
// This currently lives here because we've not yet updated our communicator
// subsystem to be aware of schema itself. Once that is done, we can remove
// this and use a type-specific schema from the communicator to validate
// exactly what is expected for a given connection type.
var ConnectionBlockSupersetSchema = &configschema.Block{
Attributes: map[string]*configschema.Attribute{
// Common attributes for both connection types
"host": {
Type: cty.String,
Required: true,
},
"type": {
Type: cty.String,
Optional: true,
},
"user": {
Type: cty.String,
Optional: true,
},
"password": {
Type: cty.String,
Optional: true,
},
"port": {
Type: cty.String,
Optional: true,
},
"timeout": {
Type: cty.String,
Optional: true,
},
"script_path": {
Type: cty.String,
Optional: true,
},
// For type=ssh only (enforced in ssh communicator)
"target_platform": {
Type: cty.String,
Optional: true,
},
"private_key": {
Type: cty.String,
Optional: true,
},
"certificate": {
Type: cty.String,
Optional: true,
},
"host_key": {
Type: cty.String,
Optional: true,
},
"agent": {
Type: cty.Bool,
Optional: true,
},
"agent_identity": {
Type: cty.String,
Optional: true,
},
"bastion_host": {
Type: cty.String,
Optional: true,
},
"bastion_host_key": {
Type: cty.String,
Optional: true,
},
"bastion_port": {
Type: cty.Number,
Optional: true,
},
"bastion_user": {
Type: cty.String,
Optional: true,
},
"bastion_password": {
Type: cty.String,
Optional: true,
},
"bastion_private_key": {
Type: cty.String,
Optional: true,
},
"bastion_certificate": {
Type: cty.String,
Optional: true,
},
// For type=winrm only (enforced in winrm communicator)
"https": {
Type: cty.Bool,
Optional: true,
},
"insecure": {
Type: cty.Bool,
Optional: true,
},
"cacert": {
Type: cty.String,
Optional: true,
},
"use_ntlm": {
Type: cty.Bool,
Optional: true,
},
},
}
// IpFormat formats the IP correctly, so we don't provide IPv6 address in an IPv4 format during node communication. We return the ip parameter as is if it's an IPv4 address or a hostname.
func IpFormat(ip string) string {
ipObj := net.ParseIP(ip)

View File

@ -20,9 +20,12 @@ import (
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/communicator/remote"
"github.com/hashicorp/terraform/internal/legacy/terraform"
"github.com/hashicorp/terraform/provisioners"
"github.com/zclconf/go-cty/cty"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
_ "github.com/hashicorp/terraform/internal/logging"
)
const (
@ -84,8 +87,8 @@ func (e fatalError) FatalError() error {
}
// New creates a new communicator implementation over SSH.
func New(s *terraform.InstanceState) (*Communicator, error) {
connInfo, err := parseConnectionInfo(s)
func New(v cty.Value) (*Communicator, error) {
connInfo, err := parseConnectionInfo(v)
if err != nil {
return nil, err
}
@ -117,7 +120,7 @@ func New(s *terraform.InstanceState) (*Communicator, error) {
}
// Connect implementation of communicator.Communicator interface
func (c *Communicator) Connect(o terraform.UIOutput) (err error) {
func (c *Communicator) Connect(o provisioners.UIOutput) (err error) {
// Grab a lock so we can modify our internal attributes
c.lock.Lock()
defer c.lock.Unlock()

View File

@ -20,7 +20,7 @@ import (
"time"
"github.com/hashicorp/terraform/communicator/remote"
"github.com/hashicorp/terraform/internal/legacy/terraform"
"github.com/zclconf/go-cty/cty"
"golang.org/x/crypto/ssh"
)
@ -125,20 +125,16 @@ func TestNew_Invalid(t *testing.T) {
address := newMockLineServer(t, nil, testClientPublicKey)
parts := strings.Split(address, ":")
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "user",
"password": "i-am-invalid",
"host": parts[0],
"port": parts[1],
"timeout": "30s",
},
},
}
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"user": cty.StringVal("user"),
"password": cty.StringVal("i-am-invalid"),
"host": cty.StringVal(parts[0]),
"port": cty.StringVal(parts[1]),
"timeout": cty.StringVal("30s"),
})
c, err := New(r)
c, err := New(v)
if err != nil {
t.Fatalf("error creating communicator: %s", err)
}
@ -150,19 +146,15 @@ func TestNew_Invalid(t *testing.T) {
}
func TestNew_InvalidHost(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "user",
"password": "i-am-invalid",
"port": "22",
"timeout": "30s",
},
},
}
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"user": cty.StringVal("user"),
"password": cty.StringVal("i-am-invalid"),
"port": cty.StringVal("22"),
"timeout": cty.StringVal("30s"),
})
_, err := New(r)
_, err := New(v)
if err == nil {
t.Fatal("should have had an error creating communicator")
}
@ -172,20 +164,16 @@ func TestStart(t *testing.T) {
address := newMockLineServer(t, nil, testClientPublicKey)
parts := strings.Split(address, ":")
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "user",
"password": "pass",
"host": parts[0],
"port": parts[1],
"timeout": "30s",
},
},
}
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"user": cty.StringVal("user"),
"password": cty.StringVal("pass"),
"host": cty.StringVal(parts[0]),
"port": cty.StringVal(parts[1]),
"timeout": cty.StringVal("30s"),
})
c, err := New(r)
c, err := New(v)
if err != nil {
t.Fatalf("error creating communicator: %s", err)
}
@ -211,19 +199,15 @@ func TestKeepAlives(t *testing.T) {
address := newMockLineServer(t, nil, testClientPublicKey)
parts := strings.Split(address, ":")
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "user",
"password": "pass",
"host": parts[0],
"port": parts[1],
},
},
}
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"user": cty.StringVal("user"),
"password": cty.StringVal("pass"),
"host": cty.StringVal(parts[0]),
"port": cty.StringVal(parts[1]),
})
c, err := New(r)
c, err := New(v)
if err != nil {
t.Fatalf("error creating communicator: %s", err)
}
@ -261,19 +245,16 @@ func TestFailedKeepAlives(t *testing.T) {
address := newMockLineServer(t, nil, testClientPublicKey)
parts := strings.Split(address, ":")
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "user",
"password": "pass",
"host": parts[0],
"port": parts[1],
},
},
}
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"user": cty.StringVal("user"),
"password": cty.StringVal("pass"),
"host": cty.StringVal(parts[0]),
"port": cty.StringVal(parts[1]),
"timeout": cty.StringVal("30s"),
})
c, err := New(r)
c, err := New(v)
if err != nil {
t.Fatalf("error creating communicator: %s", err)
}
@ -296,20 +277,16 @@ func TestLostConnection(t *testing.T) {
address := newMockLineServer(t, nil, testClientPublicKey)
parts := strings.Split(address, ":")
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "user",
"password": "pass",
"host": parts[0],
"port": parts[1],
"timeout": "30s",
},
},
}
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"user": cty.StringVal("user"),
"password": cty.StringVal("pass"),
"host": cty.StringVal(parts[0]),
"port": cty.StringVal(parts[1]),
"timeout": cty.StringVal("30s"),
})
c, err := New(r)
c, err := New(v)
if err != nil {
t.Fatalf("error creating communicator: %s", err)
}
@ -586,19 +563,15 @@ func TestAccUploadFile(t *testing.T) {
t.Skip()
}
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": os.Getenv("USER"),
"host": "127.0.0.1",
"port": "22",
"timeout": "30s",
},
},
}
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"user": cty.StringVal(os.Getenv("USER")),
"host": cty.StringVal("127.0.0.1"),
"port": cty.StringVal("22"),
"timeout": cty.StringVal("30s"),
})
c, err := New(r)
c, err := New(v)
if err != nil {
t.Fatalf("error creating communicator: %s", err)
}
@ -634,19 +607,15 @@ func TestAccHugeUploadFile(t *testing.T) {
t.Skip()
}
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": os.Getenv("USER"),
"host": "127.0.0.1",
"port": "22",
"timeout": "30s",
},
},
}
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"host": cty.StringVal("127.0.0.1"),
"user": cty.StringVal(os.Getenv("USER")),
"port": cty.StringVal("22"),
"timeout": cty.StringVal("30s"),
})
c, err := New(r)
c, err := New(v)
if err != nil {
t.Fatalf("error creating communicator: %s", err)
}
@ -706,16 +675,13 @@ func TestScriptPath(t *testing.T) {
}
for _, tc := range cases {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"host": "127.0.0.1",
"script_path": tc.Input,
},
},
}
comm, err := New(r)
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"host": cty.StringVal("127.0.0.1"),
"script_path": cty.StringVal(tc.Input),
})
comm, err := New(v)
if err != nil {
t.Fatalf("err: %s", err)
}
@ -735,14 +701,10 @@ func TestScriptPath_randSeed(t *testing.T) {
// Pre GH-4186 fix, this value was the deterministic start the pseudorandom
// chain of unseeded math/rand values for Int31().
staticSeedPath := "/tmp/terraform_1298498081.sh"
c, err := New(&terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"host": "127.0.0.1",
},
},
})
c, err := New(cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"host": cty.StringVal("127.0.0.1"),
}))
if err != nil {
t.Fatalf("err: %s", err)
}

View File

@ -10,13 +10,13 @@ import (
"net"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/hashicorp/terraform/communicator/shared"
"github.com/hashicorp/terraform/internal/legacy/terraform"
"github.com/mitchellh/mapstructure"
sshagent "github.com/xanzy/ssh-agent"
"github.com/zclconf/go-cty/cty"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"golang.org/x/crypto/ssh/knownhosts"
@ -51,41 +51,104 @@ const (
type connectionInfo struct {
User string
Password string
PrivateKey string `mapstructure:"private_key"`
Certificate string `mapstructure:"certificate"`
PrivateKey string
Certificate string
Host string
HostKey string `mapstructure:"host_key"`
HostKey string
Port int
Agent bool
ScriptPath string
TargetPlatform string
Timeout string
ScriptPath string `mapstructure:"script_path"`
TimeoutVal time.Duration `mapstructure:"-"`
TargetPlatform string `mapstructure:"target_platform"`
TimeoutVal time.Duration
BastionUser string `mapstructure:"bastion_user"`
BastionPassword string `mapstructure:"bastion_password"`
BastionPrivateKey string `mapstructure:"bastion_private_key"`
BastionCertificate string `mapstructure:"bastion_certificate"`
BastionHost string `mapstructure:"bastion_host"`
BastionHostKey string `mapstructure:"bastion_host_key"`
BastionPort int `mapstructure:"bastion_port"`
BastionUser string
BastionPassword string
BastionPrivateKey string
BastionCertificate string
BastionHost string
BastionHostKey string
BastionPort int
AgentIdentity string `mapstructure:"agent_identity"`
AgentIdentity string
}
// parseConnectionInfo is used to convert the ConnInfo of the InstanceState into
// a ConnectionInfo struct
func parseConnectionInfo(s *terraform.InstanceState) (*connectionInfo, error) {
// decodeConnInfo decodes the given cty.Value using the same behavior as the
// lgeacy mapstructure decoder in order to preserve as much of the existing
// logic as possible for compatibility.
func decodeConnInfo(v cty.Value) (*connectionInfo, error) {
connInfo := &connectionInfo{}
decConf := &mapstructure.DecoderConfig{
WeaklyTypedInput: true,
Result: connInfo,
if v.IsNull() {
return connInfo, nil
}
dec, err := mapstructure.NewDecoder(decConf)
for k, v := range v.AsValueMap() {
if v.IsNull() {
continue
}
switch k {
case "user":
connInfo.User = v.AsString()
case "password":
connInfo.Password = v.AsString()
case "private_key":
connInfo.PrivateKey = v.AsString()
case "certificate":
connInfo.Certificate = v.AsString()
case "host":
connInfo.Host = v.AsString()
case "host_key":
connInfo.HostKey = v.AsString()
case "port":
p, err := strconv.Atoi(v.AsString())
if err != nil {
return nil, err
}
connInfo.Port = p
case "agent":
connInfo.Agent = v.True()
case "script_path":
connInfo.ScriptPath = v.AsString()
case "target_platform":
connInfo.TargetPlatform = v.AsString()
case "timeout":
connInfo.Timeout = v.AsString()
case "bastion_user":
connInfo.BastionUser = v.AsString()
case "bastion_password":
connInfo.BastionPassword = v.AsString()
case "bastion_private_key":
connInfo.BastionPrivateKey = v.AsString()
case "bastion_certificate":
connInfo.BastionCertificate = v.AsString()
case "bastion_host":
connInfo.BastionHost = v.AsString()
case "bastion_host_key":
connInfo.BastionHostKey = v.AsString()
case "bastion_port":
p, err := strconv.Atoi(v.AsString())
if err != nil {
return nil, err
}
connInfo.BastionPort = p
case "agent_identity":
connInfo.AgentIdentity = v.AsString()
}
}
return connInfo, nil
}
// parseConnectionInfo is used to convert the raw configuration into the
// *connectionInfo struct.
func parseConnectionInfo(v cty.Value) (*connectionInfo, error) {
v, err := shared.ConnectionBlockSupersetSchema.CoerceValue(v)
if err != nil {
return nil, err
}
if err := dec.Decode(s.Ephemeral.ConnInfo); err != nil {
connInfo, err := decodeConnInfo(v)
if err != nil {
return nil, err
}
@ -94,7 +157,8 @@ func parseConnectionInfo(s *terraform.InstanceState) (*connectionInfo, error) {
//
// And if SSH_AUTH_SOCK is not set, there's no agent to connect to, so we
// shouldn't try.
if s.Ephemeral.ConnInfo["agent"] == "" && os.Getenv("SSH_AUTH_SOCK") != "" {
agent := v.GetAttr("agent")
if agent.IsNull() && os.Getenv("SSH_AUTH_SOCK") != "" {
connInfo.Agent = true
}

View File

@ -3,28 +3,23 @@ package ssh
import (
"testing"
"github.com/hashicorp/terraform/internal/legacy/terraform"
"github.com/zclconf/go-cty/cty"
)
func TestProvisioner_connInfo(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "root",
"password": "supersecret",
"private_key": "someprivatekeycontents",
"certificate": "somecertificate",
"host": "127.0.0.1",
"port": "22",
"timeout": "30s",
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"user": cty.StringVal("root"),
"password": cty.StringVal("supersecret"),
"private_key": cty.StringVal("someprivatekeycontents"),
"certificate": cty.StringVal("somecertificate"),
"host": cty.StringVal("127.0.0.1"),
"port": cty.StringVal("22"),
"timeout": cty.StringVal("30s"),
"bastion_host": cty.StringVal("127.0.1.1"),
})
"bastion_host": "127.0.1.1",
},
},
}
conf, err := parseConnectionInfo(r)
conf, err := parseConnectionInfo(v)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -74,24 +69,18 @@ func TestProvisioner_connInfo(t *testing.T) {
}
func TestProvisioner_connInfoIpv6(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "root",
"password": "supersecret",
"private_key": "someprivatekeycontents",
"certificate": "somecertificate",
"host": "::1",
"port": "22",
"timeout": "30s",
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"user": cty.StringVal("root"),
"password": cty.StringVal("supersecret"),
"private_key": cty.StringVal("someprivatekeycontents"),
"host": cty.StringVal("::1"),
"port": cty.StringVal("22"),
"timeout": cty.StringVal("30s"),
"bastion_host": cty.StringVal("::1"),
})
"bastion_host": "::1",
},
},
}
conf, err := parseConnectionInfo(r)
conf, err := parseConnectionInfo(v)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -106,22 +95,18 @@ func TestProvisioner_connInfoIpv6(t *testing.T) {
}
func TestProvisioner_connInfoHostname(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "root",
"password": "supersecret",
"private_key": "someprivatekeycontents",
"host": "example.com",
"port": "22",
"timeout": "30s",
"bastion_host": "example.com",
},
},
}
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"user": cty.StringVal("root"),
"password": cty.StringVal("supersecret"),
"private_key": cty.StringVal("someprivatekeycontents"),
"host": cty.StringVal("example.com"),
"port": cty.StringVal("22"),
"timeout": cty.StringVal("30s"),
"bastion_host": cty.StringVal("example.com"),
})
conf, err := parseConnectionInfo(r)
conf, err := parseConnectionInfo(v)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -136,21 +121,16 @@ func TestProvisioner_connInfoHostname(t *testing.T) {
}
func TestProvisioner_connInfoEmptyHostname(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "root",
"password": "supersecret",
"private_key": "someprivatekeycontents",
"host": "",
"port": "22",
"timeout": "30s",
},
},
}
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("ssh"),
"user": cty.StringVal("root"),
"password": cty.StringVal("supersecret"),
"private_key": cty.StringVal("someprivatekeycontents"),
"port": cty.StringVal("22"),
"timeout": cty.StringVal("30s"),
})
_, err := parseConnectionInfo(r)
_, err := parseConnectionInfo(v)
if err == nil {
t.Fatalf("bad: should not allow empty host")
}

View File

@ -10,9 +10,10 @@ import (
"time"
"github.com/hashicorp/terraform/communicator/remote"
"github.com/hashicorp/terraform/internal/legacy/terraform"
"github.com/hashicorp/terraform/provisioners"
"github.com/masterzen/winrm"
"github.com/packer-community/winrmcp/winrmcp"
"github.com/zclconf/go-cty/cty"
)
// Communicator represents the WinRM communicator
@ -24,8 +25,8 @@ type Communicator struct {
}
// New creates a new communicator implementation over WinRM.
func New(s *terraform.InstanceState) (*Communicator, error) {
connInfo, err := parseConnectionInfo(s)
func New(v cty.Value) (*Communicator, error) {
connInfo, err := parseConnectionInfo(v)
if err != nil {
return nil, err
}
@ -52,7 +53,7 @@ func New(s *terraform.InstanceState) (*Communicator, error) {
}
// Connect implementation of communicator.Communicator interface
func (c *Communicator) Connect(o terraform.UIOutput) error {
func (c *Communicator) Connect(o provisioners.UIOutput) error {
// Set the client to nil since we'll (re)create it
c.client = nil

View File

@ -9,7 +9,8 @@ import (
"github.com/dylanmei/winrmtest"
"github.com/hashicorp/terraform/communicator/remote"
"github.com/hashicorp/terraform/internal/legacy/terraform"
"github.com/hashicorp/terraform/communicator/shared"
"github.com/zclconf/go-cty/cty"
)
func newMockWinRMServer(t *testing.T) *winrmtest.Remote {
@ -47,20 +48,16 @@ func TestStart(t *testing.T) {
wrm := newMockWinRMServer(t)
defer wrm.Close()
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "winrm",
"user": "user",
"password": "pass",
"host": wrm.Host,
"port": strconv.Itoa(wrm.Port),
"timeout": "30s",
},
},
}
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("winrm"),
"user": cty.StringVal("user"),
"password": cty.StringVal("pass"),
"host": cty.StringVal(wrm.Host),
"port": cty.StringVal(strconv.Itoa(wrm.Port)),
"timeout": cty.StringVal("30s"),
})
c, err := New(r)
c, err := New(v)
if err != nil {
t.Fatalf("error creating communicator: %s", err)
}
@ -84,21 +81,16 @@ func TestStart(t *testing.T) {
func TestUpload(t *testing.T) {
wrm := newMockWinRMServer(t)
defer wrm.Close()
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("winrm"),
"user": cty.StringVal("user"),
"password": cty.StringVal("pass"),
"host": cty.StringVal(wrm.Host),
"port": cty.StringVal(strconv.Itoa(wrm.Port)),
"timeout": cty.StringVal("30s"),
})
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "winrm",
"user": "user",
"password": "pass",
"host": wrm.Host,
"port": strconv.Itoa(wrm.Port),
"timeout": "30s",
},
},
}
c, err := New(r)
c, err := New(v)
if err != nil {
t.Fatalf("error creating communicator: %s", err)
}
@ -131,15 +123,13 @@ func TestScriptPath(t *testing.T) {
}
for _, tc := range cases {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "winrm",
"script_path": tc.Input,
},
},
}
comm, err := New(r)
v := cty.ObjectVal(map[string]cty.Value{
"host": cty.StringVal(""),
"type": cty.StringVal("winrm"),
"script_path": cty.StringVal(tc.Input),
})
comm, err := New(v)
if err != nil {
t.Fatalf("err: %s", err)
}
@ -158,21 +148,16 @@ func TestScriptPath(t *testing.T) {
func TestNoTransportDecorator(t *testing.T) {
wrm := newMockWinRMServer(t)
defer wrm.Close()
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("winrm"),
"user": cty.StringVal("user"),
"password": cty.StringVal("pass"),
"host": cty.StringVal(wrm.Host),
"port": cty.StringVal(strconv.Itoa(wrm.Port)),
"timeout": cty.StringVal("30s"),
})
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "winrm",
"user": "user",
"password": "pass",
"host": wrm.Host,
"port": strconv.Itoa(wrm.Port),
"timeout": "30s",
},
},
}
c, err := New(r)
c, err := New(v)
if err != nil {
t.Fatalf("error creating communicator: %s", err)
}
@ -192,21 +177,17 @@ func TestTransportDecorator(t *testing.T) {
wrm := newMockWinRMServer(t)
defer wrm.Close()
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "winrm",
"user": "user",
"password": "pass",
"host": wrm.Host,
"port": strconv.Itoa(wrm.Port),
"use_ntlm": "true",
"timeout": "30s",
},
},
}
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("winrm"),
"user": cty.StringVal("user"),
"password": cty.StringVal("pass"),
"host": cty.StringVal(wrm.Host),
"port": cty.StringVal(strconv.Itoa(wrm.Port)),
"use_ntlm": cty.StringVal("true"),
"timeout": cty.StringVal("30s"),
})
c, err := New(r)
c, err := New(v)
if err != nil {
t.Fatalf("error creating communicator: %s", err)
}
@ -226,7 +207,7 @@ func TestScriptPath_randSeed(t *testing.T) {
// Pre GH-4186 fix, this value was the deterministic start the pseudorandom
// chain of unseeded math/rand values for Int31().
staticSeedPath := "C:/Temp/terraform_1298498081.cmd"
c, err := New(&terraform.InstanceState{})
c, err := New(cty.NullVal(shared.ConnectionBlockSupersetSchema.ImpliedType()))
if err != nil {
t.Fatalf("err: %s", err)
}

View File

@ -4,12 +4,12 @@ import (
"fmt"
"log"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/hashicorp/terraform/communicator/shared"
"github.com/hashicorp/terraform/internal/legacy/terraform"
"github.com/mitchellh/mapstructure"
"github.com/zclconf/go-cty/cty"
)
const (
@ -47,22 +47,62 @@ type connectionInfo struct {
TimeoutVal time.Duration `mapstructure:"-"`
}
// decodeConnInfo decodes the given cty.Value using the same behavior as the
// lgeacy mapstructure decoder in order to preserve as much of the existing
// logic as possible for compatibility.
func decodeConnInfo(v cty.Value) (*connectionInfo, error) {
connInfo := &connectionInfo{}
if v.IsNull() {
return connInfo, nil
}
for k, v := range v.AsValueMap() {
if v.IsNull() {
continue
}
switch k {
case "user":
connInfo.User = v.AsString()
case "password":
connInfo.Password = v.AsString()
case "host":
connInfo.Host = v.AsString()
case "port":
p, err := strconv.Atoi(v.AsString())
if err != nil {
return nil, err
}
connInfo.Port = p
case "https":
connInfo.HTTPS = v.True()
case "insecure":
connInfo.Insecure = v.True()
case "use_ntlm":
connInfo.NTLM = v.True()
case "cacert":
connInfo.CACert = v.AsString()
case "script_path":
connInfo.ScriptPath = v.AsString()
case "timeout":
connInfo.Timeout = v.AsString()
}
}
return connInfo, nil
}
// parseConnectionInfo is used to convert the ConnInfo of the InstanceState into
// a ConnectionInfo struct
func parseConnectionInfo(s *terraform.InstanceState) (*connectionInfo, error) {
connInfo := &connectionInfo{}
decConf := &mapstructure.DecoderConfig{
WeaklyTypedInput: true,
Result: connInfo,
}
dec, err := mapstructure.NewDecoder(decConf)
func parseConnectionInfo(v cty.Value) (*connectionInfo, error) {
v, err := shared.ConnectionBlockSupersetSchema.CoerceValue(v)
if err != nil {
return nil, err
}
if err := dec.Decode(s.Ephemeral.ConnInfo); err != nil {
connInfo, err := decodeConnInfo(v)
if err != nil {
return nil, err
}
// Check on script paths which point to the default Windows TEMP folder because files
// which are put in there very early in the boot process could get cleaned/deleted
// before you had the change to execute them.

View File

@ -3,23 +3,19 @@ package winrm
import (
"testing"
"github.com/hashicorp/terraform/internal/legacy/terraform"
"github.com/zclconf/go-cty/cty"
)
func TestProvisioner_defaultHTTPSPort(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "winrm",
"user": "Administrator",
"password": "supersecret",
"host": "127.0.0.1",
"https": "true",
},
},
}
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("winrm"),
"user": cty.StringVal("Administrator"),
"password": cty.StringVal("supersecret"),
"host": cty.StringVal("127.0.0.1"),
"https": cty.True,
})
conf, err := parseConnectionInfo(r)
conf, err := parseConnectionInfo(v)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -32,22 +28,18 @@ func TestProvisioner_defaultHTTPSPort(t *testing.T) {
}
func TestProvisioner_connInfo(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "winrm",
"user": "Administrator",
"password": "supersecret",
"host": "127.0.0.1",
"port": "5985",
"https": "true",
"use_ntlm": "true",
"timeout": "30s",
},
},
}
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("winrm"),
"user": cty.StringVal("Administrator"),
"password": cty.StringVal("supersecret"),
"host": cty.StringVal("127.0.0.1"),
"port": cty.StringVal("5985"),
"https": cty.True,
"use_ntlm": cty.True,
"timeout": cty.StringVal("30s"),
})
conf, err := parseConnectionInfo(r)
conf, err := parseConnectionInfo(v)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -100,23 +92,18 @@ CqDUFjhydXxYRsxXBBrEiLOE5BdtJR1sH/QHxIJe23C9iHI2nS1NbLziNEApLwC4
GnSud83VUo9G9w==
-----END CERTIFICATE-----
`
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("winrm"),
"user": cty.StringVal("Administrator"),
"password": cty.StringVal("supersecret"),
"host": cty.StringVal("127.0.0.1"),
"port": cty.StringVal("5985"),
"https": cty.True,
"timeout": cty.StringVal("30s"),
"cacert": cty.StringVal(caCert),
})
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "winrm",
"user": "Administrator",
"password": "supersecret",
"host": "127.0.0.1",
"port": "5985",
"https": "true",
"timeout": "30s",
"cacert": caCert,
},
},
}
conf, err := parseConnectionInfo(r)
conf, err := parseConnectionInfo(v)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -148,21 +135,17 @@ GnSud83VUo9G9w==
}
func TestProvisioner_connInfoIpv6(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "winrm",
"user": "Administrator",
"password": "supersecret",
"host": "::1",
"port": "5985",
"https": "true",
"timeout": "30s",
},
},
}
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("winrm"),
"user": cty.StringVal("Administrator"),
"password": cty.StringVal("supersecret"),
"host": cty.StringVal("::1"),
"port": cty.StringVal("5985"),
"https": cty.True,
"timeout": cty.StringVal("30s"),
})
conf, err := parseConnectionInfo(r)
conf, err := parseConnectionInfo(v)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -191,21 +174,17 @@ func TestProvisioner_connInfoIpv6(t *testing.T) {
}
func TestProvisioner_connInfoHostname(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "winrm",
"user": "Administrator",
"password": "supersecret",
"host": "example.com",
"port": "5985",
"https": "true",
"timeout": "30s",
},
},
}
v := cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("winrm"),
"user": cty.StringVal("Administrator"),
"password": cty.StringVal("supersecret"),
"host": cty.StringVal("example.com"),
"port": cty.StringVal("5985"),
"https": cty.True,
"timeout": cty.StringVal("30s"),
})
conf, err := parseConnectionInfo(r)
conf, err := parseConnectionInfo(v)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -235,38 +214,26 @@ func TestProvisioner_connInfoHostname(t *testing.T) {
func TestProvisioner_formatDuration(t *testing.T) {
cases := map[string]struct {
InstanceState *terraform.InstanceState
Result string
Config map[string]cty.Value
Result string
}{
"testSeconds": {
InstanceState: &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"timeout": "90s",
},
},
Config: map[string]cty.Value{
"timeout": cty.StringVal("90s"),
},
Result: "PT1M30S",
},
"testMinutes": {
InstanceState: &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"timeout": "5m",
},
},
Config: map[string]cty.Value{
"timeout": cty.StringVal("5m"),
},
Result: "PT5M",
},
"testHours": {
InstanceState: &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"timeout": "1h",
},
},
Config: map[string]cty.Value{
"timeout": cty.StringVal("1h"),
},
Result: "PT1H",
@ -274,7 +241,10 @@ func TestProvisioner_formatDuration(t *testing.T) {
}
for name, tc := range cases {
conf, err := parseConnectionInfo(tc.InstanceState)
// host is required in the schema
tc.Config["host"] = cty.StringVal("")
conf, err := parseConnectionInfo(cty.ObjectVal(tc.Config))
if err != nil {
t.Fatalf("err: %v", err)
}

View File

@ -0,0 +1,415 @@
package grpcwrap
import (
"context"
"github.com/hashicorp/terraform/internal/tfplugin5"
"github.com/hashicorp/terraform/plugin/convert"
"github.com/hashicorp/terraform/providers"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
"github.com/zclconf/go-cty/cty/msgpack"
)
// New wraps a providers.Interface to implement a grpc ProviderServer.
// This is useful for creating a test binary out of an internal provider
// implementation.
func Provider(p providers.Interface) tfplugin5.ProviderServer {
return &provider{
provider: p,
schema: p.GetSchema(),
}
}
type provider struct {
provider providers.Interface
schema providers.GetSchemaResponse
}
func (p *provider) GetSchema(_ context.Context, req *tfplugin5.GetProviderSchema_Request) (*tfplugin5.GetProviderSchema_Response, error) {
resp := &tfplugin5.GetProviderSchema_Response{
ResourceSchemas: make(map[string]*tfplugin5.Schema),
DataSourceSchemas: make(map[string]*tfplugin5.Schema),
}
resp.Provider = &tfplugin5.Schema{
Block: &tfplugin5.Schema_Block{},
}
if p.schema.Provider.Block != nil {
resp.Provider.Block = convert.ConfigSchemaToProto(p.schema.Provider.Block)
}
resp.ProviderMeta = &tfplugin5.Schema{
Block: &tfplugin5.Schema_Block{},
}
if p.schema.ProviderMeta.Block != nil {
resp.ProviderMeta.Block = convert.ConfigSchemaToProto(p.schema.ProviderMeta.Block)
}
for typ, res := range p.schema.ResourceTypes {
resp.ResourceSchemas[typ] = &tfplugin5.Schema{
Version: res.Version,
Block: convert.ConfigSchemaToProto(res.Block),
}
}
for typ, dat := range p.schema.DataSources {
resp.DataSourceSchemas[typ] = &tfplugin5.Schema{
Version: dat.Version,
Block: convert.ConfigSchemaToProto(dat.Block),
}
}
// include any diagnostics from the original GetSchema call
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, p.schema.Diagnostics)
return resp, nil
}
func (p *provider) PrepareProviderConfig(_ context.Context, req *tfplugin5.PrepareProviderConfig_Request) (*tfplugin5.PrepareProviderConfig_Response, error) {
resp := &tfplugin5.PrepareProviderConfig_Response{}
ty := p.schema.Provider.Block.ImpliedType()
configVal, err := decodeDynamicValue(req.Config, ty)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
prepareResp := p.provider.PrepareProviderConfig(providers.PrepareProviderConfigRequest{
Config: configVal,
})
// the PreparedConfig value is no longer used
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, prepareResp.Diagnostics)
return resp, nil
}
func (p *provider) ValidateResourceTypeConfig(_ context.Context, req *tfplugin5.ValidateResourceTypeConfig_Request) (*tfplugin5.ValidateResourceTypeConfig_Response, error) {
resp := &tfplugin5.ValidateResourceTypeConfig_Response{}
ty := p.schema.ResourceTypes[req.TypeName].Block.ImpliedType()
configVal, err := decodeDynamicValue(req.Config, ty)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
validateResp := p.provider.ValidateResourceTypeConfig(providers.ValidateResourceTypeConfigRequest{
TypeName: req.TypeName,
Config: configVal,
})
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, validateResp.Diagnostics)
return resp, nil
}
func (p *provider) ValidateDataSourceConfig(_ context.Context, req *tfplugin5.ValidateDataSourceConfig_Request) (*tfplugin5.ValidateDataSourceConfig_Response, error) {
resp := &tfplugin5.ValidateDataSourceConfig_Response{}
ty := p.schema.DataSources[req.TypeName].Block.ImpliedType()
configVal, err := decodeDynamicValue(req.Config, ty)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
validateResp := p.provider.ValidateDataSourceConfig(providers.ValidateDataSourceConfigRequest{
TypeName: req.TypeName,
Config: configVal,
})
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, validateResp.Diagnostics)
return resp, nil
}
func (p *provider) UpgradeResourceState(_ context.Context, req *tfplugin5.UpgradeResourceState_Request) (*tfplugin5.UpgradeResourceState_Response, error) {
resp := &tfplugin5.UpgradeResourceState_Response{}
ty := p.schema.ResourceTypes[req.TypeName].Block.ImpliedType()
upgradeResp := p.provider.UpgradeResourceState(providers.UpgradeResourceStateRequest{
TypeName: req.TypeName,
Version: req.Version,
RawStateJSON: req.RawState.Json,
})
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, upgradeResp.Diagnostics)
if upgradeResp.Diagnostics.HasErrors() {
return resp, nil
}
dv, err := encodeDynamicValue(upgradeResp.UpgradedState, ty)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
resp.UpgradedState = dv
return resp, nil
}
func (p *provider) Configure(_ context.Context, req *tfplugin5.Configure_Request) (*tfplugin5.Configure_Response, error) {
resp := &tfplugin5.Configure_Response{}
ty := p.schema.Provider.Block.ImpliedType()
configVal, err := decodeDynamicValue(req.Config, ty)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
configureResp := p.provider.Configure(providers.ConfigureRequest{
TerraformVersion: req.TerraformVersion,
Config: configVal,
})
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, configureResp.Diagnostics)
return resp, nil
}
func (p *provider) ReadResource(_ context.Context, req *tfplugin5.ReadResource_Request) (*tfplugin5.ReadResource_Response, error) {
resp := &tfplugin5.ReadResource_Response{}
ty := p.schema.ResourceTypes[req.TypeName].Block.ImpliedType()
stateVal, err := decodeDynamicValue(req.CurrentState, ty)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
metaTy := p.schema.ProviderMeta.Block.ImpliedType()
metaVal, err := decodeDynamicValue(req.ProviderMeta, metaTy)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
readResp := p.provider.ReadResource(providers.ReadResourceRequest{
TypeName: req.TypeName,
PriorState: stateVal,
Private: req.Private,
ProviderMeta: metaVal,
})
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, readResp.Diagnostics)
if readResp.Diagnostics.HasErrors() {
return resp, nil
}
resp.Private = readResp.Private
dv, err := encodeDynamicValue(readResp.NewState, ty)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
resp.NewState = dv
return resp, nil
}
func (p *provider) PlanResourceChange(_ context.Context, req *tfplugin5.PlanResourceChange_Request) (*tfplugin5.PlanResourceChange_Response, error) {
resp := &tfplugin5.PlanResourceChange_Response{}
ty := p.schema.ResourceTypes[req.TypeName].Block.ImpliedType()
priorStateVal, err := decodeDynamicValue(req.PriorState, ty)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
proposedStateVal, err := decodeDynamicValue(req.ProposedNewState, ty)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
configVal, err := decodeDynamicValue(req.Config, ty)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
metaTy := p.schema.ProviderMeta.Block.ImpliedType()
metaVal, err := decodeDynamicValue(req.ProviderMeta, metaTy)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
planResp := p.provider.PlanResourceChange(providers.PlanResourceChangeRequest{
TypeName: req.TypeName,
PriorState: priorStateVal,
ProposedNewState: proposedStateVal,
Config: configVal,
PriorPrivate: req.PriorPrivate,
ProviderMeta: metaVal,
})
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, planResp.Diagnostics)
if planResp.Diagnostics.HasErrors() {
return resp, nil
}
resp.PlannedPrivate = planResp.PlannedPrivate
resp.PlannedState, err = encodeDynamicValue(planResp.PlannedState, ty)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
for _, path := range planResp.RequiresReplace {
resp.RequiresReplace = append(resp.RequiresReplace, convert.PathToAttributePath(path))
}
return resp, nil
}
func (p *provider) ApplyResourceChange(_ context.Context, req *tfplugin5.ApplyResourceChange_Request) (*tfplugin5.ApplyResourceChange_Response, error) {
resp := &tfplugin5.ApplyResourceChange_Response{}
ty := p.schema.ResourceTypes[req.TypeName].Block.ImpliedType()
priorStateVal, err := decodeDynamicValue(req.PriorState, ty)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
plannedStateVal, err := decodeDynamicValue(req.PlannedState, ty)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
configVal, err := decodeDynamicValue(req.Config, ty)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
metaTy := p.schema.ProviderMeta.Block.ImpliedType()
metaVal, err := decodeDynamicValue(req.ProviderMeta, metaTy)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
applyResp := p.provider.ApplyResourceChange(providers.ApplyResourceChangeRequest{
TypeName: req.TypeName,
PriorState: priorStateVal,
PlannedState: plannedStateVal,
Config: configVal,
PlannedPrivate: req.PlannedPrivate,
ProviderMeta: metaVal,
})
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, applyResp.Diagnostics)
if applyResp.Diagnostics.HasErrors() {
return resp, nil
}
resp.Private = applyResp.Private
resp.NewState, err = encodeDynamicValue(applyResp.NewState, ty)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
return resp, nil
}
func (p *provider) ImportResourceState(_ context.Context, req *tfplugin5.ImportResourceState_Request) (*tfplugin5.ImportResourceState_Response, error) {
resp := &tfplugin5.ImportResourceState_Response{}
importResp := p.provider.ImportResourceState(providers.ImportResourceStateRequest{
TypeName: req.TypeName,
ID: req.Id,
})
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, importResp.Diagnostics)
for _, res := range importResp.ImportedResources {
ty := p.schema.ResourceTypes[res.TypeName].Block.ImpliedType()
state, err := encodeDynamicValue(res.State, ty)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
continue
}
resp.ImportedResources = append(resp.ImportedResources, &tfplugin5.ImportResourceState_ImportedResource{
TypeName: res.TypeName,
State: state,
Private: res.Private,
})
}
return resp, nil
}
func (p *provider) ReadDataSource(_ context.Context, req *tfplugin5.ReadDataSource_Request) (*tfplugin5.ReadDataSource_Response, error) {
resp := &tfplugin5.ReadDataSource_Response{}
ty := p.schema.DataSources[req.TypeName].Block.ImpliedType()
configVal, err := decodeDynamicValue(req.Config, ty)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
metaTy := p.schema.ProviderMeta.Block.ImpliedType()
metaVal, err := decodeDynamicValue(req.ProviderMeta, metaTy)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
readResp := p.provider.ReadDataSource(providers.ReadDataSourceRequest{
TypeName: req.TypeName,
Config: configVal,
ProviderMeta: metaVal,
})
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, readResp.Diagnostics)
if readResp.Diagnostics.HasErrors() {
return resp, nil
}
resp.State, err = encodeDynamicValue(readResp.State, ty)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
return resp, nil
}
func (p *provider) Stop(context.Context, *tfplugin5.Stop_Request) (*tfplugin5.Stop_Response, error) {
resp := &tfplugin5.Stop_Response{}
err := p.provider.Stop()
if err != nil {
resp.Error = err.Error()
}
return resp, nil
}
// decode a DynamicValue from either the JSON or MsgPack encoding.
func decodeDynamicValue(v *tfplugin5.DynamicValue, ty cty.Type) (cty.Value, error) {
// always return a valid value
var err error
res := cty.NullVal(ty)
if v == nil {
return res, nil
}
switch {
case len(v.Msgpack) > 0:
res, err = msgpack.Unmarshal(v.Msgpack, ty)
case len(v.Json) > 0:
res, err = ctyjson.Unmarshal(v.Json, ty)
}
return res, err
}
// encode a cty.Value into a DynamicValue msgpack payload.
func encodeDynamicValue(v cty.Value, ty cty.Type) (*tfplugin5.DynamicValue, error) {
mp, err := msgpack.Marshal(v, ty)
return &tfplugin5.DynamicValue{
Msgpack: mp,
}, err
}

View File

@ -0,0 +1,116 @@
package grpcwrap
import (
"context"
"log"
"strings"
"unicode/utf8"
"github.com/hashicorp/terraform/communicator/shared"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/internal/tfplugin5"
"github.com/hashicorp/terraform/plugin/convert"
"github.com/hashicorp/terraform/provisioners"
)
// New wraps a providers.Interface to implement a grpc ProviderServer.
// This is useful for creating a test binary out of an internal provider
// implementation.
func Provisioner(p provisioners.Interface) tfplugin5.ProvisionerServer {
return &provisioner{
provisioner: p,
schema: p.GetSchema().Provisioner,
}
}
type provisioner struct {
provisioner provisioners.Interface
schema *configschema.Block
}
func (p *provisioner) GetSchema(_ context.Context, req *tfplugin5.GetProvisionerSchema_Request) (*tfplugin5.GetProvisionerSchema_Response, error) {
resp := &tfplugin5.GetProvisionerSchema_Response{}
resp.Provisioner = &tfplugin5.Schema{
Block: &tfplugin5.Schema_Block{},
}
if p.schema != nil {
resp.Provisioner.Block = convert.ConfigSchemaToProto(p.schema)
}
return resp, nil
}
func (p *provisioner) ValidateProvisionerConfig(_ context.Context, req *tfplugin5.ValidateProvisionerConfig_Request) (*tfplugin5.ValidateProvisionerConfig_Response, error) {
resp := &tfplugin5.ValidateProvisionerConfig_Response{}
ty := p.schema.ImpliedType()
configVal, err := decodeDynamicValue(req.Config, ty)
if err != nil {
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
return resp, nil
}
validateResp := p.provisioner.ValidateProvisionerConfig(provisioners.ValidateProvisionerConfigRequest{
Config: configVal,
})
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, validateResp.Diagnostics)
return resp, nil
}
func (p *provisioner) ProvisionResource(req *tfplugin5.ProvisionResource_Request, srv tfplugin5.Provisioner_ProvisionResourceServer) error {
// We send back a diagnostics over the stream if there was a
// provisioner-side problem.
srvResp := &tfplugin5.ProvisionResource_Response{}
ty := p.schema.ImpliedType()
configVal, err := decodeDynamicValue(req.Config, ty)
if err != nil {
srvResp.Diagnostics = convert.AppendProtoDiag(srvResp.Diagnostics, err)
srv.Send(srvResp)
return nil
}
connVal, err := decodeDynamicValue(req.Connection, shared.ConnectionBlockSupersetSchema.ImpliedType())
if err != nil {
srvResp.Diagnostics = convert.AppendProtoDiag(srvResp.Diagnostics, err)
srv.Send(srvResp)
return nil
}
resp := p.provisioner.ProvisionResource(provisioners.ProvisionResourceRequest{
Config: configVal,
Connection: connVal,
UIOutput: uiOutput{srv},
})
srvResp.Diagnostics = convert.AppendProtoDiag(srvResp.Diagnostics, resp.Diagnostics)
srv.Send(srvResp)
return nil
}
func (p *provisioner) Stop(context.Context, *tfplugin5.Stop_Request) (*tfplugin5.Stop_Response, error) {
resp := &tfplugin5.Stop_Response{}
err := p.provisioner.Stop()
if err != nil {
resp.Error = err.Error()
}
return resp, nil
}
// uiOutput implements the terraform.UIOutput interface to adapt the grpc
// stream to the legacy Provisioner.Apply method.
type uiOutput struct {
srv tfplugin5.Provisioner_ProvisionResourceServer
}
func (o uiOutput) Output(s string) {
err := o.srv.Send(&tfplugin5.ProvisionResource_Response{
Output: strings.ToValidUTF8(s, string(utf8.RuneError)),
})
if err != nil {
log.Printf("[ERROR] %s", err)
}
}

View File

@ -1,15 +0,0 @@
package main
import (
"github.com/hashicorp/terraform/internal/legacy/builtin/providers/test"
"github.com/hashicorp/terraform/internal/legacy/terraform"
"github.com/hashicorp/terraform/plugin"
)
func main() {
plugin.Serve(&plugin.ServeOpts{
ProviderFunc: func() terraform.ResourceProvider {
return test.Provider()
},
})
}

View File

@ -1,63 +0,0 @@
package test
import (
"time"
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
)
func testDataSource() *schema.Resource {
return &schema.Resource{
Read: testDataSourceRead,
Schema: map[string]*schema.Schema{
"list": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"input": {
Type: schema.TypeString,
Optional: true,
},
"output": {
Type: schema.TypeString,
Computed: true,
},
// this attribute is computed, but never set by the provider
"nil": {
Type: schema.TypeString,
Computed: true,
},
"input_map": {
Type: schema.TypeMap,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
},
"output_map": {
Type: schema.TypeMap,
Elem: &schema.Schema{Type: schema.TypeString},
Computed: true,
},
},
}
}
func testDataSourceRead(d *schema.ResourceData, meta interface{}) error {
d.SetId(time.Now().UTC().String())
d.Set("list", []interface{}{"one", "two", "three"})
if input, hasInput := d.GetOk("input"); hasInput {
d.Set("output", input)
} else {
d.Set("output", "some output")
}
if inputMap, hasInput := d.GetOk("input_map"); hasInput {
d.Set("output_map", inputMap)
}
return nil
}

View File

@ -1,25 +0,0 @@
package test
import (
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
)
func providerLabelDataSource() *schema.Resource {
return &schema.Resource{
Read: providerLabelDataSourceRead,
Schema: map[string]*schema.Schema{
"label": {
Type: schema.TypeString,
Computed: true,
},
},
}
}
func providerLabelDataSourceRead(d *schema.ResourceData, meta interface{}) error {
label := meta.(string)
d.SetId(label)
d.Set("label", label)
return nil
}

View File

@ -1,59 +0,0 @@
package test
import (
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
"github.com/hashicorp/terraform/internal/legacy/terraform"
)
func Provider() terraform.ResourceProvider {
return &schema.Provider{
Schema: map[string]*schema.Schema{
// Optional attribute to label a particular instance for a test
// that has multiple instances of this provider, so that they
// can be distinguished using the test_provider_label data source.
"label": {
Type: schema.TypeString,
Optional: true,
},
},
ProviderMetaSchema: map[string]*schema.Schema{
// Optionally allow specifying information at a module-level
"foo": {
Type: schema.TypeString,
Optional: true,
},
},
ResourcesMap: map[string]*schema.Resource{
"test_resource": testResource(),
"test_resource_gh12183": testResourceGH12183(),
"test_resource_with_custom_diff": testResourceCustomDiff(),
"test_resource_timeout": testResourceTimeout(),
"test_resource_diff_suppress": testResourceDiffSuppress(),
"test_resource_force_new": testResourceForceNew(),
"test_resource_nested": testResourceNested(),
"test_resource_nested_set": testResourceNestedSet(),
"test_resource_state_func": testResourceStateFunc(),
"test_resource_deprecated": testResourceDeprecated(),
"test_resource_defaults": testResourceDefaults(),
"test_resource_list": testResourceList(),
"test_resource_list_set": testResourceListSet(),
"test_resource_map": testResourceMap(),
"test_resource_computed_set": testResourceComputedSet(),
"test_resource_config_mode": testResourceConfigMode(),
"test_resource_nested_id": testResourceNestedId(),
"test_resource_provider_meta": testResourceProviderMeta(),
"test_resource_signal": testResourceSignal(),
"test_undeleteable": testResourceUndeleteable(),
"test_resource_required_min": testResourceRequiredMin(),
},
DataSourcesMap: map[string]*schema.Resource{
"test_data_source": testDataSource(),
"test_provider_label": providerLabelDataSource(),
},
ConfigureFunc: providerConfigure,
}
}
func providerConfigure(d *schema.ResourceData) (interface{}, error) {
return d.Get("label"), nil
}

View File

@ -1,233 +0,0 @@
package test
import (
"errors"
"fmt"
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
)
func testResource() *schema.Resource {
return &schema.Resource{
Create: testResourceCreate,
Read: testResourceRead,
Update: testResourceUpdate,
Delete: testResourceDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
CustomizeDiff: func(d *schema.ResourceDiff, _ interface{}) error {
if d.HasChange("optional") {
d.SetNewComputed("planned_computed")
}
return nil
},
Schema: map[string]*schema.Schema{
"required": {
Type: schema.TypeString,
Required: true,
},
"optional": {
Type: schema.TypeString,
Optional: true,
},
"optional_bool": {
Type: schema.TypeBool,
Optional: true,
},
"optional_force_new": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"optional_computed_map": {
Type: schema.TypeMap,
Optional: true,
Computed: true,
},
"optional_computed_force_new": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"optional_computed": {
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"computed_read_only": {
Type: schema.TypeString,
Computed: true,
},
"computed_from_required": {
Type: schema.TypeString,
Computed: true,
ForceNew: true,
},
"computed_read_only_force_new": {
Type: schema.TypeString,
Computed: true,
ForceNew: true,
},
"computed_list": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"set": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
Set: schema.HashString,
},
"computed_set": {
Type: schema.TypeSet,
Computed: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
Set: schema.HashString,
},
"map": {
Type: schema.TypeMap,
Optional: true,
},
"optional_map": {
Type: schema.TypeMap,
Optional: true,
},
"required_map": {
Type: schema.TypeMap,
Required: true,
},
"map_that_look_like_set": {
Type: schema.TypeMap,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"computed_map": {
Type: schema.TypeMap,
Computed: true,
},
"list": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"list_of_map": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeMap,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
},
"apply_error": {
Type: schema.TypeString,
Optional: true,
Description: "return and error during apply",
},
"planned_computed": {
Type: schema.TypeString,
Computed: true,
Description: "copied the required field during apply, and plans computed when changed",
},
// this should return unset from GetOkExists
"get_ok_exists_false": {
Type: schema.TypeBool,
Computed: true,
Optional: true,
Description: "do not set in config",
},
"int": {
Type: schema.TypeInt,
Optional: true,
},
},
}
}
func testResourceCreate(d *schema.ResourceData, meta interface{}) error {
d.SetId("testId")
errMsg, _ := d.Get("apply_error").(string)
if errMsg != "" {
return errors.New(errMsg)
}
// Required must make it through to Create
if _, ok := d.GetOk("required"); !ok {
return fmt.Errorf("Missing attribute 'required', but it's required!")
}
if _, ok := d.GetOk("required_map"); !ok {
return fmt.Errorf("Missing attribute 'required_map', but it's required!")
}
d.Set("computed_from_required", d.Get("required"))
return testResourceRead(d, meta)
}
func testResourceRead(d *schema.ResourceData, meta interface{}) error {
d.Set("computed_read_only", "value_from_api")
d.Set("computed_read_only_force_new", "value_from_api")
if _, ok := d.GetOk("optional_computed_map"); !ok {
d.Set("optional_computed_map", map[string]string{})
}
d.Set("computed_map", map[string]string{"key1": "value1"})
d.Set("computed_list", []string{"listval1", "listval2"})
d.Set("computed_set", []string{"setval1", "setval2"})
d.Set("planned_computed", d.Get("optional"))
// if there is no "set" value, erroneously set it to an empty set. This
// might change a null value to an empty set, but we should be able to
// ignore that.
s := d.Get("set")
if s == nil || s.(*schema.Set).Len() == 0 {
d.Set("set", []interface{}{})
}
// This mimics many providers always setting a *string value.
// The existing behavior is that this will appear in the state as an empty
// string, which we have to maintain.
o := d.Get("optional")
if o == "" {
d.Set("optional", nil)
}
// This should not show as set unless it's set in the config
_, ok := d.GetOkExists("get_ok_exists_false")
if ok {
return errors.New("get_ok_exists_false should not be set")
}
return nil
}
func testResourceUpdate(d *schema.ResourceData, meta interface{}) error {
errMsg, _ := d.Get("apply_error").(string)
if errMsg != "" {
return errors.New(errMsg)
}
return testResourceRead(d, meta)
}
func testResourceDelete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}

View File

@ -1,123 +0,0 @@
package test
import (
"bytes"
"fmt"
"math/rand"
"strings"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
)
func testResourceComputedSet() *schema.Resource {
return &schema.Resource{
Create: testResourceComputedSetCreate,
Read: testResourceComputedSetRead,
Delete: testResourceComputedSetDelete,
Update: testResourceComputedSetUpdate,
CustomizeDiff: func(d *schema.ResourceDiff, _ interface{}) error {
o, n := d.GetChange("set_count")
if o != n {
d.SetNewComputed("string_set")
}
return nil
},
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"set_count": {
Type: schema.TypeInt,
Optional: true,
},
"string_set": {
Type: schema.TypeSet,
Computed: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
Set: schema.HashString,
},
"rule": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"id": {
Type: schema.TypeString,
Computed: true,
},
"ip_protocol": {
Type: schema.TypeString,
Required: true,
ForceNew: false,
},
"cidr": {
Type: schema.TypeString,
Optional: true,
ForceNew: false,
StateFunc: func(v interface{}) string {
return strings.ToLower(v.(string))
},
},
},
},
},
"optional_set": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
},
}
}
func computeSecGroupV2RuleHash(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%s-", m["ip_protocol"].(string)))
buf.WriteString(fmt.Sprintf("%s-", strings.ToLower(m["cidr"].(string))))
return hashcode.String(buf.String())
}
func testResourceComputedSetCreate(d *schema.ResourceData, meta interface{}) error {
d.SetId(fmt.Sprintf("%x", rand.Int63()))
return testResourceComputedSetRead(d, meta)
}
func testResourceComputedSetRead(d *schema.ResourceData, meta interface{}) error {
count := 3
v, ok := d.GetOk("set_count")
if ok {
count = v.(int)
}
var set []interface{}
for i := 0; i < count; i++ {
set = append(set, fmt.Sprintf("%d", i))
}
d.Set("string_set", schema.NewSet(schema.HashString, set))
// This isn't computed, but we should be able to ignore without issues.
d.Set("optional_set", []interface{}{})
return nil
}
func testResourceComputedSetUpdate(d *schema.ResourceData, meta interface{}) error {
return testResourceComputedSetRead(d, meta)
}
func testResourceComputedSetDelete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}

View File

@ -1,78 +0,0 @@
package test
import (
"fmt"
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
)
func testResourceConfigMode() *schema.Resource {
return &schema.Resource{
Create: testResourceConfigModeCreate,
Read: testResourceConfigModeRead,
Delete: testResourceConfigModeDelete,
Update: testResourceConfigModeUpdate,
Schema: map[string]*schema.Schema{
"resource_as_attr": {
Type: schema.TypeList,
ConfigMode: schema.SchemaConfigModeAttr,
Optional: true,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"foo": {
Type: schema.TypeString,
Optional: true,
},
},
},
},
"nested_set": {
Type: schema.TypeSet,
Optional: true,
ConfigMode: schema.SchemaConfigModeAttr,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"value": {
Type: schema.TypeString,
Optional: true,
},
"set": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
},
},
},
},
}
}
func testResourceConfigModeCreate(d *schema.ResourceData, meta interface{}) error {
d.SetId("placeholder")
return testResourceConfigModeRead(d, meta)
}
func testResourceConfigModeRead(d *schema.ResourceData, meta interface{}) error {
if l, ok := d.Get("resource_as_attr").([]interface{}); !ok {
return fmt.Errorf("resource_as_attr should appear as []interface{}, not %T", l)
} else {
for i, item := range l {
if _, ok := item.(map[string]interface{}); !ok {
return fmt.Errorf("resource_as_attr[%d] should appear as map[string]interface{}, not %T", i, item)
}
}
}
return nil
}
func testResourceConfigModeUpdate(d *schema.ResourceData, meta interface{}) error {
return testResourceConfigModeRead(d, meta)
}
func testResourceConfigModeDelete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}

View File

@ -1,70 +0,0 @@
package test
import (
"fmt"
"math/rand"
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
)
func testResourceDefaults() *schema.Resource {
return &schema.Resource{
Create: testResourceDefaultsCreate,
Read: testResourceDefaultsRead,
Delete: testResourceDefaultsDelete,
Update: testResourceDefaultsUpdate,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"default_string": {
Type: schema.TypeString,
Optional: true,
Default: "default string",
},
"default_bool": {
Type: schema.TypeString,
Optional: true,
Default: true,
},
"nested": {
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"string": {
Type: schema.TypeString,
Optional: true,
Default: "default nested",
},
"optional": {
Type: schema.TypeString,
Optional: true,
},
},
},
},
},
}
}
func testResourceDefaultsCreate(d *schema.ResourceData, meta interface{}) error {
d.SetId(fmt.Sprintf("%x", rand.Int63()))
return testResourceDefaultsRead(d, meta)
}
func testResourceDefaultsUpdate(d *schema.ResourceData, meta interface{}) error {
return testResourceDefaultsRead(d, meta)
}
func testResourceDefaultsRead(d *schema.ResourceData, meta interface{}) error {
return nil
}
func testResourceDefaultsDelete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}

View File

@ -1,119 +0,0 @@
package test
import (
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
)
func testResourceDeprecated() *schema.Resource {
return &schema.Resource{
Create: testResourceDeprecatedCreate,
Read: testResourceDeprecatedRead,
Update: testResourceDeprecatedUpdate,
Delete: testResourceDeprecatedDelete,
Schema: map[string]*schema.Schema{
"map_deprecated": {
Type: schema.TypeMap,
Optional: true,
Deprecated: "deprecated",
},
"map_removed": {
Type: schema.TypeMap,
Optional: true,
Removed: "removed",
},
"set_block_deprecated": {
Type: schema.TypeSet,
Optional: true,
MaxItems: 1,
Deprecated: "deprecated",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"value": {
Type: schema.TypeString,
Required: true,
Deprecated: "deprecated",
},
"optional": {
Type: schema.TypeString,
ForceNew: true,
Optional: true,
Deprecated: "deprecated",
},
},
},
},
"set_block_removed": {
Type: schema.TypeSet,
Optional: true,
MaxItems: 1,
Removed: "Removed",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"optional": {
Type: schema.TypeString,
ForceNew: true,
Optional: true,
Computed: true,
Removed: "removed",
},
},
},
},
"list_block_deprecated": {
Type: schema.TypeList,
Optional: true,
Deprecated: "deprecated",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"value": {
Type: schema.TypeString,
Required: true,
Deprecated: "deprecated",
},
"optional": {
Type: schema.TypeString,
ForceNew: true,
Optional: true,
Deprecated: "deprecated",
},
},
},
},
"list_block_removed": {
Type: schema.TypeList,
Optional: true,
Removed: "removed",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"optional": {
Type: schema.TypeString,
ForceNew: true,
Optional: true,
Removed: "removed",
},
},
},
},
},
}
}
func testResourceDeprecatedCreate(d *schema.ResourceData, meta interface{}) error {
d.SetId("testId")
return nil
}
func testResourceDeprecatedRead(d *schema.ResourceData, meta interface{}) error {
return nil
}
func testResourceDeprecatedUpdate(d *schema.ResourceData, meta interface{}) error {
return nil
}
func testResourceDeprecatedDelete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}

View File

@ -1,104 +0,0 @@
package test
import (
"fmt"
"math/rand"
"strings"
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
)
func testResourceDiffSuppress() *schema.Resource {
diffSuppress := func(k, old, new string, d *schema.ResourceData) bool {
if old == "" || strings.Contains(new, "replace") {
return false
}
return true
}
return &schema.Resource{
Create: testResourceDiffSuppressCreate,
Read: testResourceDiffSuppressRead,
Delete: testResourceDiffSuppressDelete,
Update: testResourceDiffSuppressUpdate,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"optional": {
Type: schema.TypeString,
Optional: true,
},
"val_to_upper": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
StateFunc: func(val interface{}) string {
return strings.ToUpper(val.(string))
},
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
return strings.ToUpper(old) == strings.ToUpper(new)
},
},
"network": {
Type: schema.TypeString,
Optional: true,
Default: "default",
ForceNew: true,
DiffSuppressFunc: diffSuppress,
},
"subnetwork": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
DiffSuppressFunc: diffSuppress,
},
"node_pool": {
Type: schema.TypeList,
Optional: true,
Computed: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
},
},
},
}
}
func testResourceDiffSuppressCreate(d *schema.ResourceData, meta interface{}) error {
d.Set("network", "modified")
d.Set("subnetwork", "modified")
if _, ok := d.GetOk("node_pool"); !ok {
d.Set("node_pool", []string{})
}
id := fmt.Sprintf("%x", rand.Int63())
d.SetId(id)
return nil
}
func testResourceDiffSuppressRead(d *schema.ResourceData, meta interface{}) error {
return nil
}
func testResourceDiffSuppressUpdate(d *schema.ResourceData, meta interface{}) error {
return nil
}
func testResourceDiffSuppressDelete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}

View File

@ -1,39 +0,0 @@
package test
import (
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
)
func testResourceForceNew() *schema.Resource {
return &schema.Resource{
Create: testResourceForceNewCreate,
Read: testResourceForceNewRead,
Delete: testResourceForceNewDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"triggers": {
Type: schema.TypeMap,
Optional: true,
ForceNew: true,
},
},
}
}
func testResourceForceNewCreate(d *schema.ResourceData, meta interface{}) error {
d.SetId("testId")
return testResourceForceNewRead(d, meta)
}
func testResourceForceNewRead(d *schema.ResourceData, meta interface{}) error {
return nil
}
func testResourceForceNewDelete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}

View File

@ -1,64 +0,0 @@
package test
import (
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
)
// This is a test resource to help reproduce GH-12183. This issue came up
// as a complex mixing of core + helper/schema and while we added core tests
// to cover some of the cases, this test helps top it off with an end-to-end
// test.
func testResourceGH12183() *schema.Resource {
return &schema.Resource{
Create: testResourceCreate_gh12183,
Read: testResourceRead_gh12183,
Update: testResourceUpdate_gh12183,
Delete: testResourceDelete_gh12183,
Schema: map[string]*schema.Schema{
"key": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"config": &schema.Schema{
Type: schema.TypeList,
Optional: true,
ForceNew: true,
MinItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
},
"rules": {
Type: schema.TypeSet,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
},
},
},
},
}
}
func testResourceCreate_gh12183(d *schema.ResourceData, meta interface{}) error {
d.SetId("testId")
return testResourceRead_gh12183(d, meta)
}
func testResourceRead_gh12183(d *schema.ResourceData, meta interface{}) error {
return nil
}
func testResourceUpdate_gh12183(d *schema.ResourceData, meta interface{}) error {
return nil
}
func testResourceDelete_gh12183(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}

View File

@ -1,192 +0,0 @@
package test
import (
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
)
func testResourceList() *schema.Resource {
return &schema.Resource{
Create: testResourceListCreate,
Read: testResourceListRead,
Update: testResourceListUpdate,
Delete: testResourceListDelete,
CustomizeDiff: func(d *schema.ResourceDiff, _ interface{}) error {
if d.HasChange("dependent_list") {
d.SetNewComputed("computed_list")
}
return nil
},
Schema: map[string]*schema.Schema{
"list_block": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"string": {
Type: schema.TypeString,
Optional: true,
},
"int": {
Type: schema.TypeInt,
Optional: true,
},
"force_new": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"sublist": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"sublist_block": {
Type: schema.TypeList,
Optional: true,
Computed: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"string": {
Type: schema.TypeString,
Required: true,
},
"int": {
Type: schema.TypeInt,
Required: true,
},
},
},
},
"sublist_block_optional": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"list": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
},
},
},
},
},
},
"dependent_list": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"val": {
Type: schema.TypeString,
Required: true,
},
},
},
},
"computed_list": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"min_items": {
Type: schema.TypeList,
Optional: true,
MinItems: 2,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"val": {
Type: schema.TypeString,
Required: true,
},
},
},
},
"never_set": {
Type: schema.TypeList,
MaxItems: 1,
Optional: true,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"sublist": {
Type: schema.TypeList,
MaxItems: 1,
ForceNew: true,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"bool": {
Type: schema.TypeBool,
ForceNew: true,
Required: true,
},
"string": {
Type: schema.TypeString,
Computed: true,
},
},
},
},
},
},
},
"map_list": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeMap},
},
},
}
}
func testResourceListCreate(d *schema.ResourceData, meta interface{}) error {
d.SetId("testId")
return testResourceListRead(d, meta)
}
func testResourceListRead(d *schema.ResourceData, meta interface{}) error {
fixedIps := d.Get("dependent_list")
// all_fixed_ips should be set as computed with a CustomizeDiff func, but
// we're trying to emulate legacy provider behavior, and updating a
// computed field was a common case.
ips := []interface{}{}
if fixedIps != nil {
for _, v := range fixedIps.([]interface{}) {
m := v.(map[string]interface{})
ips = append(ips, m["val"])
}
}
if err := d.Set("computed_list", ips); err != nil {
return err
}
// "computing" these values should insert empty containers into the
// never_set block.
values := make(map[string]interface{})
values["sublist"] = []interface{}{}
d.Set("never_set", []interface{}{values})
return nil
}
func testResourceListUpdate(d *schema.ResourceData, meta interface{}) error {
block := d.Get("never_set").([]interface{})
if len(block) > 0 {
// if profiles contains any values, they should not be nil
_ = block[0].(map[string]interface{})
}
return testResourceListRead(d, meta)
}
func testResourceListDelete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}

View File

@ -1,192 +0,0 @@
package test
import (
"fmt"
"math/rand"
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
)
func testResourceListSet() *schema.Resource {
return &schema.Resource{
Create: testResourceListSetCreate,
Read: testResourceListSetRead,
Delete: testResourceListSetDelete,
Update: testResourceListSetUpdate,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"list": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"set": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"elem": {
Type: schema.TypeString,
Optional: true,
DiffSuppressFunc: func(_, o, n string, _ *schema.ResourceData) bool {
return o == n
},
},
},
},
Set: func(v interface{}) int {
raw := v.(map[string]interface{})
if el, ok := raw["elem"]; ok {
return schema.HashString(el)
}
return 42
},
},
},
},
},
"replication_configuration": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"role": {
Type: schema.TypeString,
Required: true,
},
"rules": {
Type: schema.TypeSet,
Required: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"id": {
Type: schema.TypeString,
Optional: true,
},
"destination": {
Type: schema.TypeSet,
MaxItems: 1,
MinItems: 1,
Required: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"account_id": {
Type: schema.TypeString,
Optional: true,
},
"bucket": {
Type: schema.TypeString,
Required: true,
},
"storage_class": {
Type: schema.TypeString,
Optional: true,
},
"replica_kms_key_id": {
Type: schema.TypeString,
Optional: true,
},
"access_control_translation": {
Type: schema.TypeList,
Optional: true,
MinItems: 1,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"owner": {
Type: schema.TypeString,
Required: true,
},
},
},
},
},
},
},
"source_selection_criteria": {
Type: schema.TypeSet,
Optional: true,
MinItems: 1,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"sse_kms_encrypted_objects": {
Type: schema.TypeSet,
Optional: true,
MinItems: 1,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"enabled": {
Type: schema.TypeBool,
Required: true,
},
},
},
},
},
},
},
"prefix": {
Type: schema.TypeString,
Optional: true,
},
"status": {
Type: schema.TypeString,
Required: true,
},
"priority": {
Type: schema.TypeInt,
Optional: true,
},
"filter": {
Type: schema.TypeList,
Optional: true,
MinItems: 1,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"prefix": {
Type: schema.TypeString,
Optional: true,
},
"tags": {
Type: schema.TypeMap,
Optional: true,
},
},
},
},
},
},
},
},
},
},
},
}
}
func testResourceListSetCreate(d *schema.ResourceData, meta interface{}) error {
d.SetId(fmt.Sprintf("%x", rand.Int63()))
return testResourceListSetRead(d, meta)
}
func testResourceListSetUpdate(d *schema.ResourceData, meta interface{}) error {
return testResourceListSetRead(d, meta)
}
func testResourceListSetRead(d *schema.ResourceData, meta interface{}) error {
return nil
}
func testResourceListSetDelete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}

View File

@ -1,77 +0,0 @@
package test
import (
"fmt"
"github.com/hashicorp/terraform/configs/hcl2shim"
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
)
func testResourceMap() *schema.Resource {
return &schema.Resource{
Create: testResourceMapCreate,
Read: testResourceMapRead,
Update: testResourceMapUpdate,
Delete: testResourceMapDelete,
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
},
"map_of_three": {
Type: schema.TypeMap,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
ValidateFunc: func(v interface{}, _ string) ([]string, []error) {
errs := []error{}
for k, v := range v.(map[string]interface{}) {
if v == hcl2shim.UnknownVariableValue {
errs = append(errs, fmt.Errorf("unknown value in ValidateFunc: %q=%q", k, v))
}
}
return nil, errs
},
},
"map_values": {
Type: schema.TypeMap,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"computed_map": {
Type: schema.TypeMap,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
},
}
}
func testResourceMapCreate(d *schema.ResourceData, meta interface{}) error {
// make sure all elements are passed to the map
m := d.Get("map_of_three").(map[string]interface{})
if len(m) != 3 {
return fmt.Errorf("expected 3 map values, got %#v\n", m)
}
d.SetId("testId")
return testResourceMapRead(d, meta)
}
func testResourceMapRead(d *schema.ResourceData, meta interface{}) error {
var computedMap map[string]interface{}
if v, ok := d.GetOk("map_values"); ok {
computedMap = v.(map[string]interface{})
}
d.Set("computed_map", computedMap)
return nil
}
func testResourceMapUpdate(d *schema.ResourceData, meta interface{}) error {
return testResourceMapRead(d, meta)
}
func testResourceMapDelete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}

View File

@ -1,114 +0,0 @@
package test
import (
"fmt"
"math/rand"
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
)
func testResourceNested() *schema.Resource {
return &schema.Resource{
Create: testResourceNestedCreate,
Read: testResourceNestedRead,
Delete: testResourceNestedDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"optional": {
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
},
"nested": {
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"string": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"optional": {
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
},
"nested_again": {
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"string": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
},
},
},
},
},
},
"list_block": {
Type: schema.TypeList,
Optional: true,
Computed: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"sub_list_block": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"bool": {
Type: schema.TypeBool,
Optional: true,
},
"set": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
},
},
},
},
},
},
},
}
}
func testResourceNestedCreate(d *schema.ResourceData, meta interface{}) error {
d.SetId(fmt.Sprintf("%x", rand.Int63()))
return testResourceNestedRead(d, meta)
}
func testResourceNestedUpdate(d *schema.ResourceData, meta interface{}) error {
return testResourceNestedRead(d, meta)
}
func testResourceNestedRead(d *schema.ResourceData, meta interface{}) error {
set := []map[string]interface{}{map[string]interface{}{
"sub_list_block": []map[string]interface{}{map[string]interface{}{
"bool": false,
"set": schema.NewSet(schema.HashString, nil),
}},
}}
d.Set("list_block", set)
return nil
}
func testResourceNestedDelete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}

View File

@ -1,48 +0,0 @@
package test
import (
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
)
func testResourceNestedId() *schema.Resource {
return &schema.Resource{
Create: testResourceNestedIdCreate,
Read: testResourceNestedIdRead,
Update: testResourceNestedIdUpdate,
Delete: testResourceNestedIdDelete,
Schema: map[string]*schema.Schema{
"list_block": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"id": {
Type: schema.TypeString,
Optional: true,
Computed: true,
},
},
},
},
},
}
}
func testResourceNestedIdCreate(d *schema.ResourceData, meta interface{}) error {
d.SetId("testId")
return nil
}
func testResourceNestedIdRead(d *schema.ResourceData, meta interface{}) error {
return nil
}
func testResourceNestedIdUpdate(d *schema.ResourceData, meta interface{}) error {
return nil
}
func testResourceNestedIdDelete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}

View File

@ -1,171 +0,0 @@
package test
import (
"fmt"
"math/rand"
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
)
func testResourceNestedSet() *schema.Resource {
return &schema.Resource{
Create: testResourceNestedSetCreate,
Read: testResourceNestedSetRead,
Delete: testResourceNestedSetDelete,
Update: testResourceNestedSetUpdate,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"optional": {
Type: schema.TypeBool,
Optional: true,
},
"force_new": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"type_list": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"value": {
Type: schema.TypeString,
ForceNew: true,
Optional: true,
},
},
},
},
"single": {
Type: schema.TypeSet,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"value": {
Type: schema.TypeString,
ForceNew: true,
Required: true,
},
"optional": {
Type: schema.TypeString,
ForceNew: true,
Optional: true,
Computed: true,
},
},
},
},
"multi": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"set": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"required": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"optional_int": {
Type: schema.TypeInt,
Optional: true,
},
"bool": {
Type: schema.TypeBool,
Optional: true,
},
},
},
},
"optional": {
Type: schema.TypeString,
// commenting this causes it to get missed during apply
//ForceNew: true,
Optional: true,
},
"bool": {
Type: schema.TypeBool,
Optional: true,
},
},
},
},
"with_list": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"required": {
Type: schema.TypeString,
Required: true,
},
"list": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"list_block": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"unused": {
Type: schema.TypeString,
Optional: true,
},
},
},
},
},
},
},
},
}
}
func testResourceNestedSetCreate(d *schema.ResourceData, meta interface{}) error {
id := fmt.Sprintf("%x", rand.Int63())
d.SetId(id)
// replicate some awkward handling of a computed value in a set
set := d.Get("single").(*schema.Set)
l := set.List()
if len(l) == 1 {
if s, ok := l[0].(map[string]interface{}); ok {
if v, _ := s["optional"].(string); v == "" {
s["optional"] = id
}
}
}
d.Set("single", set)
return testResourceNestedSetRead(d, meta)
}
func testResourceNestedSetRead(d *schema.ResourceData, meta interface{}) error {
return nil
}
func testResourceNestedSetDelete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}
func testResourceNestedSetUpdate(d *schema.ResourceData, meta interface{}) error {
return nil
}

View File

@ -1,95 +0,0 @@
package test
import (
"fmt"
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
)
func testResourceProviderMeta() *schema.Resource {
return &schema.Resource{
Create: testResourceProviderMetaCreate,
Read: testResourceProviderMetaRead,
Update: testResourceProviderMetaUpdate,
Delete: testResourceProviderMetaDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"optional": {
Type: schema.TypeString,
Optional: true,
},
},
}
}
type providerMeta struct {
Foo string `cty:"foo"`
}
func testResourceProviderMetaCreate(d *schema.ResourceData, meta interface{}) error {
d.SetId("testId")
var m providerMeta
err := d.GetProviderMeta(&m)
if err != nil {
return err
}
if m.Foo != "bar" {
return fmt.Errorf("expected provider_meta.foo to be %q, was %q",
"bar", m.Foo)
}
return testResourceProviderMetaRead(d, meta)
}
func testResourceProviderMetaRead(d *schema.ResourceData, meta interface{}) error {
var m providerMeta
err := d.GetProviderMeta(&m)
if err != nil {
return err
}
if m.Foo != "bar" {
return fmt.Errorf("expected provider_meta.foo to be %q, was %q",
"bar", m.Foo)
}
return nil
}
func testResourceProviderMetaUpdate(d *schema.ResourceData, meta interface{}) error {
var m providerMeta
err := d.GetProviderMeta(&m)
if err != nil {
return err
}
if m.Foo != "bar" {
return fmt.Errorf("expected provider_meta.foo to be %q, was %q",
"bar", m.Foo)
}
return testResourceProviderMetaRead(d, meta)
}
func testResourceProviderMetaDelete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
var m providerMeta
err := d.GetProviderMeta(&m)
if err != nil {
return err
}
if m.Foo != "bar" {
return fmt.Errorf("expected provider_meta.foo to be %q, was %q",
"bar", m.Foo)
}
return nil
}

View File

@ -1,68 +0,0 @@
package test
import (
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
)
func testResourceRequiredMin() *schema.Resource {
return &schema.Resource{
Create: testResourceRequiredMinCreate,
Read: testResourceRequiredMinRead,
Update: testResourceRequiredMinUpdate,
Delete: testResourceRequiredMinDelete,
CustomizeDiff: func(d *schema.ResourceDiff, _ interface{}) error {
if d.HasChange("dependent_list") {
d.SetNewComputed("computed_list")
}
return nil
},
Schema: map[string]*schema.Schema{
"min_items": {
Type: schema.TypeList,
Optional: true,
MinItems: 2,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"val": {
Type: schema.TypeString,
Required: true,
},
},
},
},
"required_min_items": {
Type: schema.TypeList,
Required: true,
MinItems: 2,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"val": {
Type: schema.TypeString,
Required: true,
},
},
},
},
},
}
}
func testResourceRequiredMinCreate(d *schema.ResourceData, meta interface{}) error {
d.SetId("testId")
return testResourceRequiredMinRead(d, meta)
}
func testResourceRequiredMinRead(d *schema.ResourceData, meta interface{}) error {
return nil
}
func testResourceRequiredMinUpdate(d *schema.ResourceData, meta interface{}) error {
return testResourceRequiredMinRead(d, meta)
}
func testResourceRequiredMinDelete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}

View File

@ -1,43 +0,0 @@
package test
import (
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
)
func testResourceSignal() *schema.Resource {
return &schema.Resource{
Create: testResourceSignalCreate,
Read: testResourceSignalRead,
Update: testResourceSignalUpdate,
Delete: testResourceSignalDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"optional": {
Type: schema.TypeString,
Optional: true,
},
},
}
}
func testResourceSignalCreate(d *schema.ResourceData, meta interface{}) error {
d.SetId("testId")
return testResourceSignalRead(d, meta)
}
func testResourceSignalRead(d *schema.ResourceData, meta interface{}) error {
return nil
}
func testResourceSignalUpdate(d *schema.ResourceData, meta interface{}) error {
return testResourceSignalRead(d, meta)
}
func testResourceSignalDelete(d *schema.ResourceData, meta interface{}) error {
return nil
}

View File

@ -1,118 +0,0 @@
package test
import (
"crypto/sha1"
"encoding/hex"
"fmt"
"math/rand"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
)
func testResourceStateFunc() *schema.Resource {
return &schema.Resource{
Create: testResourceStateFuncCreate,
Read: testResourceStateFuncRead,
Update: testResourceStateFuncUpdate,
Delete: testResourceStateFuncDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"optional": {
Type: schema.TypeString,
Optional: true,
},
"state_func": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
StateFunc: stateFuncHash,
},
"state_func_value": {
Type: schema.TypeString,
Optional: true,
},
// set block with computed elements
"set_block": {
Type: schema.TypeSet,
Optional: true,
Set: setBlockHash,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"required": {
Type: schema.TypeString,
Required: true,
},
"optional": {
Type: schema.TypeString,
Optional: true,
Computed: true,
},
},
},
},
},
}
}
func stateFuncHash(v interface{}) string {
hash := sha1.Sum([]byte(v.(string)))
return hex.EncodeToString(hash[:])
}
func setBlockHash(v interface{}) int {
m := v.(map[string]interface{})
required, _ := m["required"].(string)
optional, _ := m["optional"].(string)
return hashcode.String(fmt.Sprintf("%s|%s", required, optional))
}
func testResourceStateFuncCreate(d *schema.ResourceData, meta interface{}) error {
d.SetId(fmt.Sprintf("%x", rand.Int63()))
// if we have a reference for the actual data in the state_func field,
// compare it
if data, ok := d.GetOk("state_func_value"); ok {
expected := data.(string)
got := d.Get("state_func").(string)
if expected != got {
return fmt.Errorf("expected state_func value:%q, got%q", expected, got)
}
}
// Check that we can lookup set elements by our computed hash.
// This is not advised, but we can use this to make sure the final diff was
// prepared with the correct values.
setBlock, ok := d.GetOk("set_block")
if ok {
set := setBlock.(*schema.Set)
for _, obj := range set.List() {
idx := setBlockHash(obj)
requiredAddr := fmt.Sprintf("%s.%d.%s", "set_block", idx, "required")
_, ok := d.GetOkExists(requiredAddr)
if !ok {
return fmt.Errorf("failed to get attr %q from %#v", fmt.Sprintf(requiredAddr), d.State().Attributes)
}
}
}
return testResourceStateFuncRead(d, meta)
}
func testResourceStateFuncRead(d *schema.ResourceData, meta interface{}) error {
return nil
}
func testResourceStateFuncUpdate(d *schema.ResourceData, meta interface{}) error {
return nil
}
func testResourceStateFuncDelete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}

View File

@ -1,125 +0,0 @@
package test
import (
"fmt"
"time"
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
)
func testResourceTimeout() *schema.Resource {
return &schema.Resource{
Create: testResourceTimeoutCreate,
Read: testResourceTimeoutRead,
Update: testResourceTimeoutUpdate,
Delete: testResourceTimeoutDelete,
// Due to the schema version also being stashed in the private/meta
// data, we need to ensure that it does not overwrite the map
// containing the timeouts.
SchemaVersion: 1,
Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(time.Second),
Update: schema.DefaultTimeout(time.Second),
Delete: schema.DefaultTimeout(time.Second),
},
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"create_delay": {
Type: schema.TypeString,
Optional: true,
},
"read_delay": {
Type: schema.TypeString,
Optional: true,
},
"update_delay": {
Type: schema.TypeString,
Optional: true,
},
"delete_delay": {
Type: schema.TypeString,
Optional: true,
},
},
}
}
func testResourceTimeoutCreate(d *schema.ResourceData, meta interface{}) error {
delayString := d.Get("create_delay").(string)
var delay time.Duration
var err error
if delayString != "" {
delay, err = time.ParseDuration(delayString)
if err != nil {
return err
}
}
if delay > d.Timeout(schema.TimeoutCreate) {
return fmt.Errorf("timeout while creating resource")
}
d.SetId("testId")
return testResourceRead(d, meta)
}
func testResourceTimeoutRead(d *schema.ResourceData, meta interface{}) error {
delayString := d.Get("read_delay").(string)
var delay time.Duration
var err error
if delayString != "" {
delay, err = time.ParseDuration(delayString)
if err != nil {
return err
}
}
if delay > d.Timeout(schema.TimeoutRead) {
return fmt.Errorf("timeout while reading resource")
}
return nil
}
func testResourceTimeoutUpdate(d *schema.ResourceData, meta interface{}) error {
delayString := d.Get("update_delay").(string)
var delay time.Duration
var err error
if delayString != "" {
delay, err = time.ParseDuration(delayString)
if err != nil {
return err
}
}
if delay > d.Timeout(schema.TimeoutUpdate) {
return fmt.Errorf("timeout while updating resource")
}
return nil
}
func testResourceTimeoutDelete(d *schema.ResourceData, meta interface{}) error {
delayString := d.Get("delete_delay").(string)
var delay time.Duration
var err error
if delayString != "" {
delay, err = time.ParseDuration(delayString)
if err != nil {
return err
}
}
if delay > d.Timeout(schema.TimeoutDelete) {
return fmt.Errorf("timeout while deleting resource")
}
d.SetId("")
return nil
}

View File

@ -1,30 +0,0 @@
package test
import (
"fmt"
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
)
func testResourceUndeleteable() *schema.Resource {
return &schema.Resource{
Create: testResourceUndeleteableCreate,
Read: testResourceUndeleteableRead,
Delete: testResourceUndeleteableDelete,
Schema: map[string]*schema.Schema{},
}
}
func testResourceUndeleteableCreate(d *schema.ResourceData, meta interface{}) error {
d.SetId("placeholder")
return testResourceUndeleteableRead(d, meta)
}
func testResourceUndeleteableRead(d *schema.ResourceData, meta interface{}) error {
return nil
}
func testResourceUndeleteableDelete(d *schema.ResourceData, meta interface{}) error {
return fmt.Errorf("test_undeleteable always fails deletion (use terraform state rm if you really want to delete it)")
}

View File

@ -1,154 +0,0 @@
package test
import (
"fmt"
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
)
func testResourceCustomDiff() *schema.Resource {
return &schema.Resource{
Create: testResourceCustomDiffCreate,
Read: testResourceCustomDiffRead,
CustomizeDiff: testResourceCustomDiffCustomizeDiff,
Update: testResourceCustomDiffUpdate,
Delete: testResourceCustomDiffDelete,
Schema: map[string]*schema.Schema{
"required": {
Type: schema.TypeString,
Required: true,
},
"computed": {
Type: schema.TypeInt,
Computed: true,
},
"index": {
Type: schema.TypeInt,
Computed: true,
},
"veto": {
Type: schema.TypeBool,
Optional: true,
},
"list": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
},
}
}
type listDiffCases struct {
Type string
Value string
}
func testListDiffCases(index int) []listDiffCases {
switch index {
case 0:
return []listDiffCases{
{
Type: "add",
Value: "dc1",
},
}
case 1:
return []listDiffCases{
{
Type: "remove",
Value: "dc1",
},
{
Type: "add",
Value: "dc2",
},
{
Type: "add",
Value: "dc3",
},
}
}
return nil
}
func testListDiffCasesReadResult(index int) []interface{} {
switch index {
case 1:
return []interface{}{"dc1"}
default:
return []interface{}{"dc2", "dc3"}
}
}
func testResourceCustomDiffCreate(d *schema.ResourceData, meta interface{}) error {
d.SetId("testId")
// Required must make it through to Create
if _, ok := d.GetOk("required"); !ok {
return fmt.Errorf("missing attribute 'required', but it's required")
}
_, new := d.GetChange("computed")
expected := new.(int) - 1
actual := d.Get("index").(int)
if expected != actual {
return fmt.Errorf("expected computed to be 1 ahead of index, got computed: %d, index: %d", expected, actual)
}
d.Set("index", new)
return testResourceCustomDiffRead(d, meta)
}
func testResourceCustomDiffRead(d *schema.ResourceData, meta interface{}) error {
if err := d.Set("list", testListDiffCasesReadResult(d.Get("index").(int))); err != nil {
return err
}
return nil
}
func testResourceCustomDiffCustomizeDiff(d *schema.ResourceDiff, meta interface{}) error {
if d.Get("veto").(bool) == true {
return fmt.Errorf("veto is true, diff vetoed")
}
// Note that this gets put into state after the update, regardless of whether
// or not anything is acted upon in the diff.
d.SetNew("computed", d.Get("computed").(int)+1)
// This tests a diffed list, based off of the value of index
dcs := testListDiffCases(d.Get("index").(int))
s := d.Get("list").([]interface{})
for _, dc := range dcs {
switch dc.Type {
case "add":
s = append(s, dc.Value)
case "remove":
for i := range s {
if s[i].(string) == dc.Value {
copy(s[i:], s[i+1:])
s = s[:len(s)-1]
break
}
}
}
}
d.SetNew("list", s)
return nil
}
func testResourceCustomDiffUpdate(d *schema.ResourceData, meta interface{}) error {
_, new := d.GetChange("computed")
expected := new.(int) - 1
actual := d.Get("index").(int)
if expected != actual {
return fmt.Errorf("expected computed to be 1 ahead of index, got computed: %d, index: %d", expected, actual)
}
d.Set("index", new)
return testResourceCustomDiffRead(d, meta)
}
func testResourceCustomDiffDelete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}

View File

@ -0,0 +1,16 @@
package main
import (
"github.com/hashicorp/terraform/internal/grpcwrap"
simple "github.com/hashicorp/terraform/internal/provider-simple"
"github.com/hashicorp/terraform/internal/tfplugin5"
"github.com/hashicorp/terraform/plugin"
)
func main() {
plugin.Serve(&plugin.ServeOpts{
GRPCProviderFunc: func() tfplugin5.ProviderServer {
return grpcwrap.Provider(simple.Provider())
},
})
}

View File

@ -0,0 +1,128 @@
// simple provider a minimal provider implementation for testing
package simple
import (
"errors"
"time"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/providers"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
)
type simple struct {
schema providers.GetSchemaResponse
}
func Provider() providers.Interface {
simpleResource := providers.Schema{
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Computed: true,
Type: cty.String,
},
"value": {
Optional: true,
Type: cty.String,
},
},
},
}
return simple{
schema: providers.GetSchemaResponse{
Provider: providers.Schema{
Block: nil,
},
ResourceTypes: map[string]providers.Schema{
"simple_resource": simpleResource,
},
DataSources: map[string]providers.Schema{
"simple_resource": simpleResource,
},
},
}
}
func (s simple) GetSchema() providers.GetSchemaResponse {
return s.schema
}
func (s simple) PrepareProviderConfig(req providers.PrepareProviderConfigRequest) (resp providers.PrepareProviderConfigResponse) {
return resp
}
func (s simple) ValidateResourceTypeConfig(req providers.ValidateResourceTypeConfigRequest) (resp providers.ValidateResourceTypeConfigResponse) {
return resp
}
func (s simple) ValidateDataSourceConfig(req providers.ValidateDataSourceConfigRequest) (resp providers.ValidateDataSourceConfigResponse) {
return resp
}
func (p simple) UpgradeResourceState(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) {
ty := p.schema.ResourceTypes[req.TypeName].Block.ImpliedType()
val, err := ctyjson.Unmarshal(req.RawStateJSON, ty)
resp.Diagnostics = resp.Diagnostics.Append(err)
resp.UpgradedState = val
return resp
}
func (s simple) Configure(providers.ConfigureRequest) (resp providers.ConfigureResponse) {
return resp
}
func (s simple) Stop() error {
return nil
}
func (s simple) ReadResource(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) {
// just return the same state we received
resp.NewState = req.PriorState
return resp
}
func (s simple) PlanResourceChange(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
m := req.ProposedNewState.AsValueMap()
_, ok := m["id"]
if !ok {
m["id"] = cty.UnknownVal(cty.String)
}
resp.PlannedState = cty.ObjectVal(m)
return resp
}
func (s simple) ApplyResourceChange(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
if req.PlannedState.IsNull() {
resp.NewState = req.PlannedState
return resp
}
m := req.PlannedState.AsValueMap()
_, ok := m["id"]
if !ok {
m["id"] = cty.StringVal(time.Now().String())
}
resp.NewState = cty.ObjectVal(m)
return resp
}
func (s simple) ImportResourceState(providers.ImportResourceStateRequest) (resp providers.ImportResourceStateResponse) {
resp.Diagnostics = resp.Diagnostics.Append(errors.New("unsupported"))
return resp
}
func (s simple) ReadDataSource(req providers.ReadDataSourceRequest) (resp providers.ReadDataSourceResponse) {
m := req.Config.AsValueMap()
m["id"] = cty.StringVal("static_id")
resp.State = cty.ObjectVal(m)
return resp
}
func (s simple) Close() error {
return nil
}

View File

@ -0,0 +1,17 @@
package main
import (
"github.com/hashicorp/terraform/builtin/providers/terraform"
"github.com/hashicorp/terraform/internal/grpcwrap"
"github.com/hashicorp/terraform/internal/tfplugin5"
"github.com/hashicorp/terraform/plugin"
)
func main() {
// Provide a binary version of the internal terraform provider for testing
plugin.Serve(&plugin.ServeOpts{
GRPCProviderFunc: func() tfplugin5.ProviderServer {
return grpcwrap.Provider(terraform.NewProvider())
},
})
}

View File

@ -0,0 +1,17 @@
package main
import (
localexec "github.com/hashicorp/terraform/builtin/provisioners/local-exec"
"github.com/hashicorp/terraform/internal/grpcwrap"
"github.com/hashicorp/terraform/internal/tfplugin5"
"github.com/hashicorp/terraform/plugin"
)
func main() {
// Provide a binary version of the internal terraform provider for testing
plugin.Serve(&plugin.ServeOpts{
GRPCProvisionerFunc: func() tfplugin5.ProvisionerServer {
return grpcwrap.Provisioner(localexec.New())
},
})
}

View File

@ -1,285 +0,0 @@
// 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() {
if isProjectRoot() == false {
log.Fatalf("This program must be invoked in the terraform project root")
}
//// 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(nil, 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
}
func isProjectRoot() bool {
_, err := os.Stat("go.mod")
if os.IsNotExist(err) {
return false
}
return true
}
// makeProvisionerMap creates a map of provisioners like this:
//
// "chef": chefprovisioner.Provisioner,
// "salt-masterless": saltmasterlessprovisioner.Provisioner,
// "file": fileprovisioner.Provisioner,
// "local-exec": localexecprovisioner.Provisioner,
// "remote-exec": remoteexecprovisioner.Provisioner,
//
func makeProvisionerMap(items []plugin) string {
output := ""
for _, item := range items {
output += fmt.Sprintf("\t\"%s\": %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() == "testdata" {
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 := "terraform.ResourceProvisioner"
typeName := "Provisioner"
return discoverTypesInPath(path, typeID, typeName)
}
const source = `//
// This file is automatically generated by scripts/generate-plugins.go -- Do not edit!
//
package command
import (
IMPORTS
"github.com/hashicorp/terraform/plugin"
)
var InternalProviders = map[string]plugin.ProviderFunc{}
var InternalProvisioners = map[string]plugin.ProvisionerFunc{
PROVISIONERS
}
`

View File

@ -1,99 +0,0 @@
package main
import "testing"
func TestMakeProvisionerMap(t *testing.T) {
p := makeProvisionerMap([]plugin{
{
Package: "file",
PluginName: "file",
TypeName: "Provisioner",
Path: "builtin/provisioners/file",
ImportName: "fileprovisioner",
},
{
Package: "localexec",
PluginName: "local-exec",
TypeName: "Provisioner",
Path: "builtin/provisioners/local-exec",
ImportName: "localexecprovisioner",
},
{
Package: "remoteexec",
PluginName: "remote-exec",
TypeName: "Provisioner",
Path: "builtin/provisioners/remote-exec",
ImportName: "remoteexecprovisioner",
},
})
expected := ` "file": fileprovisioner.Provisioner,
"local-exec": localexecprovisioner.Provisioner,
"remote-exec": remoteexecprovisioner.Provisioner,
`
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", "terraform.ResourceProvisioner", "Provisioner")
if err != nil {
t.Fatalf(err.Error())
}
if !contains(plugins, "remote-exec") {
t.Errorf("Expected to find remote-exec provisioner")
}
if contains(plugins, "aws") {
t.Errorf("Found unexpected provisioner aws")
}
}

View File

@ -1,10 +0,0 @@
package terraform
import (
"github.com/hashicorp/terraform/version"
)
// Deprecated: Providers should use schema.Provider.TerraformVersion instead
func VersionString() string {
return version.String()
}