rewrite file as an internal provisioner

This commit is contained in:
James Bardin 2020-11-25 17:06:17 -05:00
parent 1ec8d921d4
commit 256a7ec95a
2 changed files with 147 additions and 115 deletions

View File

@ -2,96 +2,131 @@ package file
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"sync"
"github.com/hashicorp/terraform/communicator" "github.com/hashicorp/terraform/communicator"
"github.com/hashicorp/terraform/internal/legacy/helper/schema" "github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/internal/legacy/terraform" "github.com/hashicorp/terraform/provisioners"
"github.com/mitchellh/go-homedir" "github.com/mitchellh/go-homedir"
"github.com/zclconf/go-cty/cty"
) )
func Provisioner() terraform.ResourceProvisioner { func New() provisioners.Interface {
return &schema.Provisioner{ return &provisioner{}
Schema: map[string]*schema.Schema{ }
"source": &schema.Schema{
Type: schema.TypeString, type provisioner struct {
Optional: true, // this stored from the running context, so that Stop() can
ConflictsWith: []string{"content"}, // 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{ "content": {
Type: schema.TypeString, Type: cty.String,
Optional: true, Optional: true,
ConflictsWith: []string{"source"},
}, },
"destination": &schema.Schema{ "destination": {
Type: schema.TypeString, Type: cty.String,
Required: true, Required: true,
}, },
}, },
ApplyFunc: applyFn,
ValidateFunc: validateFn,
} }
resp.Provisioner = schema
return resp
} }
func applyFn(ctx context.Context) error { func (p *provisioner) ValidateProvisionerConfig(req provisioners.ValidateProvisionerConfigRequest) (resp provisioners.ValidateProvisionerConfigResponse) {
connState := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState) cfg, err := p.GetSchema().Provisioner.CoerceValue(req.Config)
data := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
// Get a new communicator
comm, err := communicator.New(connState)
if err != nil { 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 // Get the source
src, deleteSource, err := getSrc(data) src, deleteSource, err := getSrc(req.Config)
if err != nil { if err != nil {
return err resp.Diagnostics = resp.Diagnostics.Append(err)
return resp
} }
if deleteSource { if deleteSource {
defer os.Remove(src) defer os.Remove(src)
} }
// Begin the file copy // Begin the file copy
dst := data.Get("destination").(string) dst := req.Config.GetAttr("destination").AsString()
if err := copyFiles(ctx, comm, src, dst); err != nil { if err := copyFiles(ctx, comm, src, dst); err != nil {
return err resp.Diagnostics = resp.Diagnostics.Append(err)
} return resp
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'"))
} }
return ws, es return resp
} }
// getSrc returns the file to use as source // getSrc returns the file to use as source
func getSrc(data *schema.ResourceData) (string, bool, error) { func getSrc(v cty.Value) (string, bool, error) {
src := data.Get("source").(string) content := v.GetAttr("content")
if content, ok := data.GetOk("content"); ok { src := v.GetAttr("source")
switch {
case !content.IsNull():
file, err := ioutil.TempFile("", "tf-file-content") file, err := ioutil.TempFile("", "tf-file-content")
if err != nil { if err != nil {
return "", true, err return "", true, err
} }
if _, err = file.WriteString(content.(string)); err != nil { if _, err = file.WriteString(content.AsString()); err != nil {
return "", true, err return "", true, err
} }
return file.Name(), true, nil return file.Name(), true, nil
}
expansion, err := homedir.Expand(src) case !src.IsNull():
return expansion, false, err 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 // 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 { if err != nil {
return fmt.Errorf("Upload failed: %v", err) return fmt.Errorf("Upload failed: %v", err)
} }
return 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 ( import (
"testing" "testing"
"github.com/hashicorp/terraform/configs/hcl2shim" "github.com/hashicorp/terraform/provisioners"
"github.com/hashicorp/terraform/internal/legacy/helper/schema" "github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/legacy/terraform"
) )
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) { func TestResourceProvider_Validate_good_source(t *testing.T) {
c := testConfig(t, map[string]interface{}{ v := cty.ObjectVal(map[string]cty.Value{
"source": "/tmp/foo", "source": cty.StringVal("/tmp/foo"),
"destination": "/tmp/bar", "destination": cty.StringVal("/tmp/bar"),
}) })
warn, errs := Provisioner().Validate(c) resp := New().ValidateProvisionerConfig(provisioners.ValidateProvisionerConfigRequest{
if len(warn) > 0 { Config: v,
t.Fatalf("Warnings: %v", warn) })
}
if len(errs) > 0 { if len(resp.Diagnostics) > 0 {
t.Fatalf("Errors: %v", errs) t.Fatal(resp.Diagnostics.ErrWithWarnings())
} }
} }
func TestResourceProvider_Validate_good_content(t *testing.T) { func TestResourceProvider_Validate_good_content(t *testing.T) {
c := testConfig(t, map[string]interface{}{ v := cty.ObjectVal(map[string]cty.Value{
"content": "value to copy", "content": cty.StringVal("value to copy"),
"destination": "/tmp/bar", "destination": cty.StringVal("/tmp/bar"),
}) })
warn, errs := Provisioner().Validate(c) resp := New().ValidateProvisionerConfig(provisioners.ValidateProvisionerConfigRequest{
if len(warn) > 0 { Config: v,
t.Fatalf("Warnings: %v", warn) })
}
if len(errs) > 0 { if len(resp.Diagnostics) > 0 {
t.Fatalf("Errors: %v", errs) t.Fatal(resp.Diagnostics.ErrWithWarnings())
} }
} }
func TestResourceProvider_Validate_good_unknown_variable_value(t *testing.T) { func TestResourceProvider_Validate_good_unknown_variable_value(t *testing.T) {
c := testConfig(t, map[string]interface{}{ v := cty.ObjectVal(map[string]cty.Value{
"content": hcl2shim.UnknownVariableValue, "content": cty.UnknownVal(cty.String),
"destination": "/tmp/bar", "destination": cty.StringVal("/tmp/bar"),
}) })
warn, errs := Provisioner().Validate(c) resp := New().ValidateProvisionerConfig(provisioners.ValidateProvisionerConfigRequest{
if len(warn) > 0 { Config: v,
t.Fatalf("Warnings: %v", warn) })
}
if len(errs) > 0 { if len(resp.Diagnostics) > 0 {
t.Fatalf("Errors: %v", errs) t.Fatal(resp.Diagnostics.ErrWithWarnings())
} }
} }
func TestResourceProvider_Validate_bad_not_destination(t *testing.T) { func TestResourceProvider_Validate_bad_not_destination(t *testing.T) {
c := testConfig(t, map[string]interface{}{ v := cty.ObjectVal(map[string]cty.Value{
"source": "nope", "source": cty.StringVal("nope"),
}) })
warn, errs := Provisioner().Validate(c) resp := New().ValidateProvisionerConfig(provisioners.ValidateProvisionerConfigRequest{
if len(warn) > 0 { Config: v,
t.Fatalf("Warnings: %v", warn) })
}
if len(errs) == 0 { if !resp.Diagnostics.HasErrors() {
t.Fatalf("Should have errors") t.Fatal("Should have errors")
} }
} }
func TestResourceProvider_Validate_bad_no_source(t *testing.T) { func TestResourceProvider_Validate_bad_no_source(t *testing.T) {
c := testConfig(t, map[string]interface{}{ v := cty.ObjectVal(map[string]cty.Value{
"destination": "/tmp/bar", "destination": cty.StringVal("/tmp/bar"),
}) })
warn, errs := Provisioner().Validate(c) resp := New().ValidateProvisionerConfig(provisioners.ValidateProvisionerConfigRequest{
if len(warn) > 0 { Config: v,
t.Fatalf("Warnings: %v", warn) })
}
if len(errs) == 0 { if !resp.Diagnostics.HasErrors() {
t.Fatalf("Should have errors") t.Fatal("Should have errors")
} }
} }
func TestResourceProvider_Validate_bad_to_many_src(t *testing.T) { func TestResourceProvider_Validate_bad_to_many_src(t *testing.T) {
c := testConfig(t, map[string]interface{}{ v := cty.ObjectVal(map[string]cty.Value{
"source": "nope", "source": cty.StringVal("nope"),
"content": "value to copy", "content": cty.StringVal("vlue to copy"),
"destination": "/tmp/bar", "destination": cty.StringVal("/tmp/bar"),
}) })
warn, errs := Provisioner().Validate(c) resp := New().ValidateProvisionerConfig(provisioners.ValidateProvisionerConfigRequest{
if len(warn) > 0 { Config: v,
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 { if !resp.Diagnostics.HasErrors() {
return terraform.NewResourceConfigRaw(c) t.Fatal("Should have errors")
}
} }