From 256a7ec95a188f37465c8832bb817a5e785f081c Mon Sep 17 00:00:00 2001 From: James Bardin Date: Wed, 25 Nov 2020 17:06:17 -0500 Subject: [PATCH] rewrite file as an internal provisioner --- .../provisioners/file/resource_provisioner.go | 137 ++++++++++++------ .../file/resource_provisioner_test.go | 125 +++++++--------- 2 files changed, 147 insertions(+), 115 deletions(-) diff --git a/builtin/provisioners/file/resource_provisioner.go b/builtin/provisioners/file/resource_provisioner.go index 61941e978..559be022d 100644 --- a/builtin/provisioners/file/resource_provisioner.go +++ b/builtin/provisioners/file/resource_provisioner.go @@ -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 +} diff --git a/builtin/provisioners/file/resource_provisioner_test.go b/builtin/provisioners/file/resource_provisioner_test.go index f56f7641c..9532fd517 100644 --- a/builtin/provisioners/file/resource_provisioner_test.go +++ b/builtin/provisioners/file/resource_provisioner_test.go @@ -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") + } }