Merge pull request #28753 from hashicorp/jbardin/provisioner-diags

return diagnostics from provisioners
This commit is contained in:
James Bardin 2021-05-19 12:54:08 -04:00 committed by GitHub
commit a4ed1405f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 87 additions and 42 deletions

View File

@ -10,6 +10,7 @@ import (
"github.com/hashicorp/terraform/internal/communicator" "github.com/hashicorp/terraform/internal/communicator"
"github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/provisioners" "github.com/hashicorp/terraform/internal/provisioners"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/mitchellh/go-homedir" "github.com/mitchellh/go-homedir"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
) )
@ -75,20 +76,32 @@ func (p *provisioner) ValidateProvisionerConfig(req provisioners.ValidateProvisi
func (p *provisioner) ProvisionResource(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { func (p *provisioner) ProvisionResource(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
if req.Connection.IsNull() { if req.Connection.IsNull() {
resp.Diagnostics = resp.Diagnostics.Append(errors.New("missing connection configuration for provisioner")) resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
tfdiags.Error,
"file provisioner error",
"Missing connection configuration for provisioner.",
))
return resp return resp
} }
comm, err := communicator.New(req.Connection) comm, err := communicator.New(req.Connection)
if err != nil { if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err) resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
tfdiags.Error,
"file provisioner error",
err.Error(),
))
return resp return resp
} }
// Get the source // Get the source
src, deleteSource, err := getSrc(req.Config) src, deleteSource, err := getSrc(req.Config)
if err != nil { if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err) resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
tfdiags.Error,
"file provisioner error",
err.Error(),
))
return resp return resp
} }
if deleteSource { if deleteSource {
@ -98,7 +111,11 @@ func (p *provisioner) ProvisionResource(req provisioners.ProvisionResourceReques
// Begin the file copy // Begin the file copy
dst := req.Config.GetAttr("destination").AsString() dst := req.Config.GetAttr("destination").AsString()
if err := copyFiles(p.ctx, comm, src, dst); err != nil { if err := copyFiles(p.ctx, comm, src, dst); err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err) resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
tfdiags.Error,
"file provisioner error",
err.Error(),
))
return resp return resp
} }

View File

@ -112,7 +112,7 @@ func TestResourceProvisioner_connectionRequired(t *testing.T) {
} }
got := resp.Diagnostics.Err().Error() got := resp.Diagnostics.Err().Error()
if !strings.Contains(got, "missing connection") { if !strings.Contains(got, "Missing connection") {
t.Fatalf("expected 'missing connection' error: got %q", got) t.Fatalf("expected 'Missing connection' error: got %q", got)
} }
} }

View File

@ -11,6 +11,7 @@ import (
"github.com/armon/circbuf" "github.com/armon/circbuf"
"github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/provisioners" "github.com/hashicorp/terraform/internal/provisioners"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/mitchellh/go-linereader" "github.com/mitchellh/go-linereader"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
) )
@ -65,7 +66,11 @@ func (p *provisioner) GetSchema() (resp provisioners.GetSchemaResponse) {
func (p *provisioner) ValidateProvisionerConfig(req provisioners.ValidateProvisionerConfigRequest) (resp provisioners.ValidateProvisionerConfigResponse) { func (p *provisioner) ValidateProvisionerConfig(req provisioners.ValidateProvisionerConfigRequest) (resp provisioners.ValidateProvisionerConfigResponse) {
if _, err := p.GetSchema().Provisioner.CoerceValue(req.Config); err != nil { if _, err := p.GetSchema().Provisioner.CoerceValue(req.Config); err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err) resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
tfdiags.Error,
"Invalid local-exec provisioner configuration",
err.Error(),
))
} }
return resp return resp
} }
@ -73,7 +78,11 @@ func (p *provisioner) ValidateProvisionerConfig(req provisioners.ValidateProvisi
func (p *provisioner) ProvisionResource(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { func (p *provisioner) ProvisionResource(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
command := req.Config.GetAttr("command").AsString() command := req.Config.GetAttr("command").AsString()
if command == "" { if command == "" {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("local-exec provisioner command must be a non-empty string")) resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
tfdiags.Error,
"Invalid local-exec provisioner command",
"The command must be a non-empty string.",
))
return resp return resp
} }
@ -120,7 +129,11 @@ func (p *provisioner) ProvisionResource(req provisioners.ProvisionResourceReques
// See golang.org/issue/18874 // See golang.org/issue/18874
pr, pw, err := os.Pipe() pr, pw, err := os.Pipe()
if err != nil { if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("failed to initialize pipe for output: %s", err)) resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
tfdiags.Error,
"local-exec provisioner error",
fmt.Sprintf("Failed to initialize pipe for output: %s", err),
))
return resp return resp
} }
@ -171,8 +184,11 @@ func (p *provisioner) ProvisionResource(req provisioners.ProvisionResourceReques
} }
if err != nil { if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("Error running command '%s': %v. Output: %s", resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
command, err, output.Bytes())) tfdiags.Error,
"local-exec provisioner error",
fmt.Sprintf("Error running command '%s': %v. Output: %s", command, err, output.Bytes()),
))
return resp return resp
} }

View File

@ -15,6 +15,7 @@ import (
"github.com/hashicorp/terraform/internal/communicator/remote" "github.com/hashicorp/terraform/internal/communicator/remote"
"github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/provisioners" "github.com/hashicorp/terraform/internal/provisioners"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/mitchellh/go-linereader" "github.com/mitchellh/go-linereader"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
) )
@ -59,7 +60,11 @@ func (p *provisioner) GetSchema() (resp provisioners.GetSchemaResponse) {
func (p *provisioner) ValidateProvisionerConfig(req provisioners.ValidateProvisionerConfigRequest) (resp provisioners.ValidateProvisionerConfigResponse) { func (p *provisioner) ValidateProvisionerConfig(req provisioners.ValidateProvisionerConfigRequest) (resp provisioners.ValidateProvisionerConfigResponse) {
cfg, err := p.GetSchema().Provisioner.CoerceValue(req.Config) cfg, err := p.GetSchema().Provisioner.CoerceValue(req.Config)
if err != nil { if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err) resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
tfdiags.Error,
"Invalid remote-exec provisioner configuration",
err.Error(),
))
return resp return resp
} }
@ -78,28 +83,43 @@ func (p *provisioner) ValidateProvisionerConfig(req provisioners.ValidateProvisi
set++ set++
} }
if set != 1 { if set != 1 {
resp.Diagnostics = resp.Diagnostics.Append(errors.New( resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
`only one of "inline", "script", or "scripts" must be set`)) tfdiags.Error,
"Invalid remote-exec provisioner configuration",
`Only one of "inline", "script", or "scripts" must be set`,
))
} }
return resp return resp
} }
func (p *provisioner) ProvisionResource(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { func (p *provisioner) ProvisionResource(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
if req.Connection.IsNull() { if req.Connection.IsNull() {
resp.Diagnostics = resp.Diagnostics.Append(errors.New("missing connection configuration for provisioner")) resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
tfdiags.Error,
"remote-exec provisioner error",
"Missing connection configuration for provisioner.",
))
return resp return resp
} }
comm, err := communicator.New(req.Connection) comm, err := communicator.New(req.Connection)
if err != nil { if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err) resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
tfdiags.Error,
"remote-exec provisioner error",
err.Error(),
))
return resp return resp
} }
// Collect the scripts // Collect the scripts
scripts, err := collectScripts(req.Config) scripts, err := collectScripts(req.Config)
if err != nil { if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err) resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
tfdiags.Error,
"remote-exec provisioner error",
err.Error(),
))
return resp return resp
} }
for _, s := range scripts { for _, s := range scripts {
@ -108,7 +128,11 @@ func (p *provisioner) ProvisionResource(req provisioners.ProvisionResourceReques
// Copy and execute each script // Copy and execute each script
if err := runScripts(p.ctx, req.UIOutput, comm, scripts); err != nil { if err := runScripts(p.ctx, req.UIOutput, comm, scripts); err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err) resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
tfdiags.Error,
"remote-exec provisioner error",
err.Error(),
))
return resp return resp
} }

View File

@ -271,8 +271,8 @@ func TestResourceProvisioner_connectionRequired(t *testing.T) {
} }
got := resp.Diagnostics.Err().Error() got := resp.Diagnostics.Err().Error()
if !strings.Contains(got, "missing connection") { if !strings.Contains(got, "Missing connection") {
t.Fatalf("expected 'missing connection' error: got %q", got) t.Fatalf("expected 'Missing connection' error: got %q", got)
} }
} }

View File

@ -1701,9 +1701,8 @@ func (n *NodeAbstractResourceInstance) evalApplyProvisioners(ctx EvalContext, st
// If there are no errors, then we append it to our output error // If there are no errors, then we append it to our output error
// if we have one, otherwise we just output it. // if we have one, otherwise we just output it.
err := n.applyProvisioners(ctx, state, when, provs) diags = diags.Append(n.applyProvisioners(ctx, state, when, provs))
if err != nil { if diags.HasErrors() {
diags = diags.Append(err)
log.Printf("[TRACE] evalApplyProvisioners: %s provisioning failed, but we will continue anyway at the caller's request", n.Addr) log.Printf("[TRACE] evalApplyProvisioners: %s provisioning failed, but we will continue anyway at the caller's request", n.Addr)
return diags return diags
} }
@ -1737,7 +1736,7 @@ func filterProvisioners(config *configs.Resource, when configs.ProvisionerWhen)
} }
// applyProvisioners executes the provisioners for a resource. // applyProvisioners executes the provisioners for a resource.
func (n *NodeAbstractResourceInstance) applyProvisioners(ctx EvalContext, state *states.ResourceInstanceObject, when configs.ProvisionerWhen, provs []*configs.Provisioner) error { func (n *NodeAbstractResourceInstance) applyProvisioners(ctx EvalContext, state *states.ResourceInstanceObject, when configs.ProvisionerWhen, provs []*configs.Provisioner) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics var diags tfdiags.Diagnostics
// this self is only used for destroy provisioner evaluation, and must // this self is only used for destroy provisioner evaluation, and must
@ -1766,8 +1765,7 @@ func (n *NodeAbstractResourceInstance) applyProvisioners(ctx EvalContext, state
// Get the provisioner // Get the provisioner
provisioner, err := ctx.Provisioner(prov.Type) provisioner, err := ctx.Provisioner(prov.Type)
if err != nil { if err != nil {
diags = diags.Append(err) return diags.Append(err)
return diags.Err()
} }
schema := ctx.ProvisionerSchema(prov.Type) schema := ctx.ProvisionerSchema(prov.Type)
@ -1775,7 +1773,7 @@ func (n *NodeAbstractResourceInstance) applyProvisioners(ctx EvalContext, state
config, configDiags := evalScope(ctx, prov.Config, self, schema) config, configDiags := evalScope(ctx, prov.Config, self, schema)
diags = diags.Append(configDiags) diags = diags.Append(configDiags)
if diags.HasErrors() { if diags.HasErrors() {
return diags.Err() return diags
} }
// If the provisioner block contains a connection block of its own then // If the provisioner block contains a connection block of its own then
@ -1807,7 +1805,7 @@ func (n *NodeAbstractResourceInstance) applyProvisioners(ctx EvalContext, state
connInfo, connInfoDiags = evalScope(ctx, connBody, self, connectionBlockSupersetSchema) connInfo, connInfoDiags = evalScope(ctx, connBody, self, connectionBlockSupersetSchema)
diags = diags.Append(connInfoDiags) diags = diags.Append(connInfoDiags)
if diags.HasErrors() { if diags.HasErrors() {
return diags.Err() return diags
} }
} }
@ -1817,7 +1815,7 @@ func (n *NodeAbstractResourceInstance) applyProvisioners(ctx EvalContext, state
return h.PreProvisionInstanceStep(n.Addr, prov.Type) return h.PreProvisionInstanceStep(n.Addr, prov.Type)
}) })
if err != nil { if err != nil {
return err return diags.Append(err)
} }
} }
@ -1874,27 +1872,17 @@ func (n *NodeAbstractResourceInstance) applyProvisioners(ctx EvalContext, state
diags = diags.Append(applyDiags) diags = diags.Append(applyDiags)
if applyDiags.HasErrors() { if applyDiags.HasErrors() {
log.Printf("[WARN] Errors while provisioning %s with %q, so aborting", n.Addr, prov.Type) log.Printf("[WARN] Errors while provisioning %s with %q, so aborting", n.Addr, prov.Type)
return diags.Err() return diags
} }
} }
// Deal with the hook // Deal with the hook
if hookErr != nil { if hookErr != nil {
return hookErr return diags.Append(hookErr)
} }
} }
// we have to drop warning-only diagnostics for now return diags
if diags.HasErrors() {
return diags.ErrWithWarnings()
}
// log any warnings since we can't return them
if e := diags.ErrWithWarnings(); e != nil {
log.Printf("[WARN] applyProvisioners %s: %v", n.Addr, e)
}
return nil
} }
func (n *NodeAbstractResourceInstance) evalProvisionerConfig(ctx EvalContext, body hcl.Body, self cty.Value, schema *configschema.Block) (cty.Value, tfdiags.Diagnostics) { func (n *NodeAbstractResourceInstance) evalProvisionerConfig(ctx EvalContext, body hcl.Body, self cty.Value, schema *configschema.Block) (cty.Value, tfdiags.Diagnostics) {