Bump github.com/hashicorp/terraform-plugin-sdk/v2 from 2.20.0 to 2.24.1

Bumps [github.com/hashicorp/terraform-plugin-sdk/v2](https://github.com/hashicorp/terraform-plugin-sdk) from 2.20.0 to 2.24.1.
- [Release notes](https://github.com/hashicorp/terraform-plugin-sdk/releases)
- [Changelog](https://github.com/hashicorp/terraform-plugin-sdk/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/terraform-plugin-sdk/compare/v2.20.0...v2.24.1)

---
updated-dependencies:
- dependency-name: github.com/hashicorp/terraform-plugin-sdk/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
dependabot[bot]
2022-12-24 16:57:19 +00:00
committed by Tobias Trabelsi
parent 683a051502
commit 282cd097f9
195 changed files with 3914 additions and 3093 deletions

25
vendor/github.com/hashicorp/go-plugin/CHANGELOG.md generated vendored Normal file
View File

@ -0,0 +1,25 @@
## v1.4.6
BUG FIXES:
* server: Prevent gRPC broker goroutine leak when using `GRPCServer` type `GracefulStop()` or `Stop()` methods [[GH-220](https://github.com/hashicorp/go-plugin/pull/220)]
## v1.4.5
ENHANCEMENTS:
* client: log warning when SecureConfig is nil [[GH-207](https://github.com/hashicorp/go-plugin/pull/207)]
## v1.4.4
ENHANCEMENTS:
* client: increase level of plugin exit logs [[GH-195](https://github.com/hashicorp/go-plugin/pull/195)]
BUG FIXES:
* Bidirectional communication: fix bidirectional communication when AutoMTLS is enabled [[GH-193](https://github.com/hashicorp/go-plugin/pull/193)]
* RPC: Trim a spurious log message for plugins using RPC [[GH-186](https://github.com/hashicorp/go-plugin/pull/186)]

View File

@ -1,3 +1,5 @@
Copyright (c) 2016 HashiCorp, Inc.
Mozilla Public License, version 2.0
1. Definitions

View File

@ -547,7 +547,9 @@ func (c *Client) Start() (addr net.Addr, err error) {
return nil, err
}
if c.config.SecureConfig != nil {
if c.config.SecureConfig == nil {
c.logger.Warn("plugin configured with a nil SecureConfig")
} else {
if ok, err := c.config.SecureConfig.Check(cmd.Path); err != nil {
return nil, fmt.Errorf("error verifying checksum: %s", err)
} else if !ok {

View File

@ -107,14 +107,26 @@ func (s *GRPCServer) Init() error {
return nil
}
// Stop calls Stop on the underlying grpc.Server
// Stop calls Stop on the underlying grpc.Server and Close on the underlying
// grpc.Broker if present.
func (s *GRPCServer) Stop() {
s.server.Stop()
if s.broker != nil {
s.broker.Close()
s.broker = nil
}
}
// GracefulStop calls GracefulStop on the underlying grpc.Server
// GracefulStop calls GracefulStop on the underlying grpc.Server and Close on
// the underlying grpc.Broker if present.
func (s *GRPCServer) GracefulStop() {
s.server.GracefulStop()
if s.broker != nil {
s.broker.Close()
s.broker = nil
}
}
// Config is the GRPCServerConfig encoded as JSON then base64.

View File

@ -42,6 +42,8 @@ func (s *RPCServer) Config() string { return "" }
// ServerProtocol impl.
func (s *RPCServer) Serve(lis net.Listener) {
defer s.done()
for {
conn, err := lis.Accept()
if err != nil {
@ -82,7 +84,7 @@ func (s *RPCServer) ServeConn(conn io.ReadWriteCloser) {
// Connect the stdstreams (in, out, err)
stdstream := make([]net.Conn, 2)
for i, _ := range stdstream {
for i := range stdstream {
stdstream[i], err = mux.Accept()
if err != nil {
mux.Close()
@ -133,13 +135,15 @@ type controlServer struct {
// Ping can be called to verify the connection (and likely the binary)
// is still alive to a plugin.
func (c *controlServer) Ping(
null bool, response *struct{}) error {
null bool, response *struct{},
) error {
*response = struct{}{}
return nil
}
func (c *controlServer) Quit(
null bool, response *struct{}) error {
null bool, response *struct{},
) error {
// End the server
c.server.done()
@ -156,7 +160,8 @@ type dispenseServer struct {
}
func (d *dispenseServer) Dispense(
name string, response *uint32) error {
name string, response *uint32,
) error {
// Find the function to create this implementation
p, ok := d.plugins[name]
if !ok {

View File

@ -1,10 +1,35 @@
# HCL Changelog
## v2.15.0 (November 10, 2022)
### Bugs Fixed
* ext/typeexpr: Skip null objects when applying defaults. This prevents crashes when null objects are creating inside collections, and stops incomplete objects being created with only optional attributes set. ([#567](https://github.com/hashicorp/hcl/pull/567))
* ext/typeexpr: Ensure default values do not have optional metadata attached. This prevents crashes when default values are inserted into concrete go-cty values that have also been stripped of their optional metadata. ([#568](https://github.com/hashicorp/hcl/pull/568))
### Enhancements
* ext/typeexpr: With the [go-cty](https://github.com/zclconf/go-cty) upstream depenendency updated to v1.12.0, the `Defaults` struct and associated functions can apply additional and more flexible 'unsafe' conversions (examples include tuples into collections such as lists and sets, and additional safety around null and dynamic values). ([#564](https://github.com/hashicorp/hcl/pull/564))
* ext/typeexpr: With the [go-cty](https://github.com/zclconf/go-cty) upstream depenendency updated to v1.12.0, users should now apply the go-cty convert functionality *before* setting defaults on a given `cty.Value`, rather than after, if they require a specific `cty.Type`. ([#564](https://github.com/hashicorp/hcl/pull/564))
## v2.14.1 (September 23, 2022)
### Bugs Fixed
* ext/typeexpr: Type convert defaults for optional object attributes when applying them. This prevents crashes in certain cases when the objects in question are part of a collection. ([#555](https://github.com/hashicorp/hcl/pull/555))
## v2.14.0 (September 1, 2022)
### Enhancements
* ext/typeexpr: Added support for optional object attributes to `TypeConstraint`. Attributes can be wrapped in the special `optional(…)` modifier, allowing the attribute to be omitted while still meeting the type constraint. For more information, [cty's documentation on conversion between object types](https://github.com/zclconf/go-cty/blob/main/docs/convert.md#conversion-between-object-types). ([#549](https://github.com/hashicorp/hcl/pull/549))
* ext/typeexpr: New function: `TypeConstraintWithDefaults`. In this mode, the `optional(…)` modifier accepts a second argument which can be used as the default value for omitted object attributes. The function returns both a `cty.Type` and associated `Defaults`, the latter of which has an `Apply` method to apply defaults to a given value. ([#549](https://github.com/hashicorp/hcl/pull/549))
## v2.13.0 (June 22, 2022)
### Enhancements
* hcl: `hcl.Diagnostic` how has an additional field `Extra` which is intended for carrying arbitrary supporting data ("extra information") related to the diagnostic message, intended to allow diagnostic renderers to optionally tailor the presentation of messages for particular situations. ([#539](https://github.com/hashicorp/hcl/pull/539))
* hcl: `hcl.Diagnostic` now has an additional field `Extra` which is intended for carrying arbitrary supporting data ("extra information") related to the diagnostic message, intended to allow diagnostic renderers to optionally tailor the presentation of messages for particular situations. ([#539](https://github.com/hashicorp/hcl/pull/539))
* hclsyntax: When an error occurs during a function call, the returned diagnostics will include _extra information_ (as described in the previous point) about which function was being called and, if the message is about an error returned by the function itself, that raw `error` value without any post-processing. ([#539](https://github.com/hashicorp/hcl/pull/539))
### Bugs Fixed

View File

@ -84,7 +84,7 @@ Comments serve as program documentation and come in two forms:
sequence, and may have any characters within except the ending sequence.
An inline comments is considered equivalent to a whitespace sequence.
Comments and whitespace cannot begin within within other comments, or within
Comments and whitespace cannot begin within other comments, or within
template literals except inside an interpolation sequence or template directive.
### Identifiers

View File

@ -1,6 +1,6 @@
package version
const version = "0.17.2"
const version = "0.17.3"
// ModuleVersion returns the current version of the github.com/hashicorp/terraform-exec Go module.
// This is a function to allow for future possible enhancement using debug.BuildInfo.

View File

@ -46,6 +46,7 @@ var (
statePlanReadErrRegexp = regexp.MustCompile(
`Terraform couldn't read the given file as a state or plan file.|` +
`Error: Failed to read the given file as a state or plan file`)
lockIdInvalidErrRegexp = regexp.MustCompile(`Failed to unlock state: `)
)
func (tf *Terraform) wrapExitError(ctx context.Context, err error, stderr string) error {
@ -160,6 +161,8 @@ func (tf *Terraform) wrapExitError(ctx context.Context, err error, stderr string
}
case statePlanReadErrRegexp.MatchString(stderr):
return &ErrStatePlanRead{stderr: stderr}
case lockIdInvalidErrRegexp.MatchString(stderr):
return &ErrLockIdInvalid{stderr: stderr}
}
return fmt.Errorf("%w\n%s", &unwrapper{exitErr, ctxErr}, stderr)
@ -256,6 +259,16 @@ func (e *ErrNoConfig) Error() string {
return e.stderr
}
type ErrLockIdInvalid struct {
unwrapper
stderr string
}
func (e *ErrLockIdInvalid) Error() string {
return e.stderr
}
// ErrCLIUsage is returned when the combination of flags or arguments is incorrect.
//
// CLI indicates usage errors in three different ways: either

View File

@ -2,6 +2,7 @@ package tfexec
import (
"context"
"fmt"
"os/exec"
)
@ -21,7 +22,10 @@ func (opt *DirOption) configureForceUnlock(conf *forceUnlockConfig) {
// ForceUnlock represents the `terraform force-unlock` command
func (tf *Terraform) ForceUnlock(ctx context.Context, lockID string, opts ...ForceUnlockOption) error {
unlockCmd := tf.forceUnlockCmd(ctx, lockID, opts...)
unlockCmd, err := tf.forceUnlockCmd(ctx, lockID, opts...)
if err != nil {
return err
}
if err := tf.runTerraformCmd(ctx, unlockCmd); err != nil {
return err
@ -30,21 +34,25 @@ func (tf *Terraform) ForceUnlock(ctx context.Context, lockID string, opts ...For
return nil
}
func (tf *Terraform) forceUnlockCmd(ctx context.Context, lockID string, opts ...ForceUnlockOption) *exec.Cmd {
func (tf *Terraform) forceUnlockCmd(ctx context.Context, lockID string, opts ...ForceUnlockOption) (*exec.Cmd, error) {
c := defaultForceUnlockOptions
for _, o := range opts {
o.configureForceUnlock(&c)
}
args := []string{"force-unlock", "-force"}
args := []string{"force-unlock", "-no-color", "-force"}
// positional arguments
args = append(args, lockID)
// optional positional arguments
if c.dir != "" {
err := tf.compatible(ctx, nil, tf0_15_0)
if err != nil {
return nil, fmt.Errorf("[DIR] option was removed in Terraform v0.15.0")
}
args = append(args, c.dir)
}
return tf.buildTerraformCmd(ctx, nil, args...)
return tf.buildTerraformCmd(ctx, nil, args...), nil
}

View File

@ -52,6 +52,10 @@ func (opt *DirOption) configureInit(conf *initConfig) {
conf.dir = opt.path
}
func (opt *ForceCopyOption) configureInit(conf *initConfig) {
conf.forceCopy = opt.forceCopy
}
func (opt *FromModuleOption) configureInit(conf *initConfig) {
conf.fromModule = opt.source
}
@ -116,7 +120,7 @@ func (tf *Terraform) initCmd(ctx context.Context, opts ...InitOption) (*exec.Cmd
o.configureInit(&c)
}
args := []string{"init", "-no-color", "-force-copy", "-input=false"}
args := []string{"init", "-no-color", "-input=false"}
// string opts: only pass if set
if c.fromModule != "" {
@ -144,6 +148,10 @@ func (tf *Terraform) initCmd(ctx context.Context, opts ...InitOption) (*exec.Cmd
args = append(args, "-verify-plugins="+fmt.Sprint(c.verifyPlugins))
}
if c.forceCopy {
args = append(args, "-force-copy")
}
// unary flags: pass if true
if c.reconfigure {
args = append(args, "-reconfigure")

View File

@ -1,3 +1,5 @@
Copyright (c) 2020 HashiCorp, Inc.
Mozilla Public License, version 2.0
1. Definitions

View File

@ -25,3 +25,9 @@ func ProtocolWarn(ctx context.Context, msg string, additionalFields ...map[strin
func ProtocolTrace(ctx context.Context, msg string, additionalFields ...map[string]interface{}) {
tfsdklog.SubsystemTrace(ctx, SubsystemProto, msg, additionalFields...)
}
// ProtocolSetField returns a context with the additional protocol subsystem
// field set.
func ProtocolSetField(ctx context.Context, key string, value any) context.Context {
return tfsdklog.SubsystemSetField(ctx, SubsystemProto, key, value)
}

View File

@ -55,20 +55,34 @@ func ProtocolData(ctx context.Context, dataDir string, rpc string, message strin
return
}
fileName := fmt.Sprintf("%d_%s_%s_%s.%s", time.Now().Unix(), rpc, message, field, fileExtension)
filePath := path.Join(dataDir, fileName)
logFields := map[string]interface{}{KeyProtocolDataFile: filePath} // should not be persisted using With()
writeProtocolFile(ctx, dataDir, rpc, message, field, fileExtension, fileContents)
}
ProtocolTrace(ctx, "Writing protocol data file", logFields)
// ProtocolPrivateData emits raw protocol private data to a file, if given a
// directory. This data is "private" in the sense that it is provider-owned,
// rather than something managed by Terraform.
//
// The directory must exist and be writable, prior to invoking this function.
//
// File names are in the format: {TIME}_{RPC}_{MESSAGE}_{FIELD}(.empty)
func ProtocolPrivateData(ctx context.Context, dataDir string, rpc string, message string, field string, data []byte) {
if dataDir == "" {
// Write a log, only once, that explains how to enable this functionality.
protocolDataSkippedLog.Do(func() {
ProtocolTrace(ctx, "Skipping protocol data file writing because no data directory is set. "+
fmt.Sprintf("Use the %s environment variable to enable this functionality.", EnvTfLogSdkProtoDataDir))
})
err := os.WriteFile(filePath, fileContents, 0644)
if err != nil {
ProtocolError(ctx, fmt.Sprintf("Unable to write protocol data file: %s", err), logFields)
return
}
ProtocolTrace(ctx, "Wrote protocol data file", logFields)
var fileExtension string
if len(data) == 0 {
fileExtension = fileExtEmpty
}
writeProtocolFile(ctx, dataDir, rpc, message, field, fileExtension, data)
}
func protocolDataDynamicValue5(_ context.Context, value *tfprotov5.DynamicValue) (string, []byte) {
@ -106,3 +120,25 @@ func protocolDataDynamicValue6(_ context.Context, value *tfprotov6.DynamicValue)
return fileExtEmpty, nil
}
func writeProtocolFile(ctx context.Context, dataDir string, rpc string, message string, field string, fileExtension string, fileContents []byte) {
fileName := fmt.Sprintf("%d_%s_%s_%s", time.Now().Unix(), rpc, message, field)
if fileExtension != "" {
fileName += "." + fileExtension
}
filePath := path.Join(dataDir, fileName)
ctx = ProtocolSetField(ctx, KeyProtocolDataFile, filePath)
ProtocolTrace(ctx, "Writing protocol data file")
err := os.WriteFile(filePath, fileContents, 0644)
if err != nil {
ProtocolError(ctx, "Unable to write protocol data file", map[string]any{KeyError: err.Error()})
return
}
ProtocolTrace(ctx, "Wrote protocol data file")
}

View File

@ -31,10 +31,9 @@ func (d Diagnostics) ErrorCount() int {
// Log will log every diagnostic:
//
// - Error severity at ERROR level
// - Warning severity at WARN level
// - Invalid/Unknown severity at WARN level
//
// - Error severity at ERROR level
// - Warning severity at WARN level
// - Invalid/Unknown severity at WARN level
func (d Diagnostics) Log(ctx context.Context) {
for _, diagnostic := range d {
if diagnostic == nil {

View File

@ -21,10 +21,9 @@ func DownstreamRequest(ctx context.Context) context.Context {
// DownstreamResponse generates the following logging:
//
// - TRACE "Received downstream response" log with request duration and
// diagnostic severity counts
// - Per-diagnostic logs
//
// - TRACE "Received downstream response" log with request duration and
// diagnostic severity counts
// - Per-diagnostic logs
func DownstreamResponse(ctx context.Context, diagnostics diag.Diagnostics) {
responseFields := map[string]interface{}{
logging.KeyDiagnosticErrorCount: diagnostics.ErrorCount(),

View File

@ -19,7 +19,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc-gen-go v1.28.1
// protoc v3.19.4
// source: tfplugin5.proto
@ -1795,6 +1795,15 @@ func (x *PrepareProviderConfig_Response) GetDiagnostics() []*Diagnostic {
return nil
}
// Request is the message that is sent to the provider during the
// UpgradeResourceState RPC.
//
// This message intentionally does not include configuration data as any
// configuration-based or configuration-conditional changes should occur
// during the PlanResourceChange RPC. Additionally, the configuration is
// not guaranteed to exist (in the case of resource destruction), be wholly
// known, nor match the given prior state, which could lead to unexpected
// provider behaviors for practitioners.
type UpgradeResourceState_Request struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@ -2231,6 +2240,14 @@ func (x *Configure_Response) GetDiagnostics() []*Diagnostic {
return nil
}
// Request is the message that is sent to the provider during the
// ReadResource RPC.
//
// This message intentionally does not include configuration data as any
// configuration-based or configuration-conditional changes should occur
// during the PlanResourceChange RPC. Additionally, the configuration is
// not guaranteed to be wholly known nor match the given prior state, which
// could lead to unexpected provider behaviors for practitioners.
type ReadResource_Request struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache

View File

@ -183,6 +183,15 @@ message PrepareProviderConfig {
}
message UpgradeResourceState {
// Request is the message that is sent to the provider during the
// UpgradeResourceState RPC.
//
// This message intentionally does not include configuration data as any
// configuration-based or configuration-conditional changes should occur
// during the PlanResourceChange RPC. Additionally, the configuration is
// not guaranteed to exist (in the case of resource destruction), be wholly
// known, nor match the given prior state, which could lead to unexpected
// provider behaviors for practitioners.
message Request {
string type_name = 1;
@ -240,6 +249,14 @@ message Configure {
}
message ReadResource {
// Request is the message that is sent to the provider during the
// ReadResource RPC.
//
// This message intentionally does not include configuration data as any
// configuration-based or configuration-conditional changes should occur
// during the PlanResourceChange RPC. Additionally, the configuration is
// not guaranteed to be wholly known nor match the given prior state, which
// could lead to unexpected provider behaviors for practitioners.
message Request {
string type_name = 1;
DynamicValue current_state = 2;

View File

@ -54,7 +54,7 @@ type GetProviderSchemaResponse struct {
// will be specified in the provider block of the user's configuration.
Provider *Schema
// ProviderMeta defines the schema for the provider's metadta, which
// ProviderMeta defines the schema for the provider's metadata, which
// will be specified in the provider_meta blocks of the terraform block
// for a module. This is an advanced feature and its usage should be
// coordinated with the Terraform Core team by opening an issue at

View File

@ -77,3 +77,22 @@ func (s RawState) Unmarshal(typ tftypes.Type) (tftypes.Value, error) {
}
return tftypes.Value{}, ErrUnknownRawStateType
}
// UnmarshalOpts contains options that can be used to modify the behaviour when
// unmarshalling. Currently, this only contains a struct for opts for JSON but
// could have a field for Flatmap in the future.
type UnmarshalOpts struct {
ValueFromJSONOpts tftypes.ValueFromJSONOpts
}
// UnmarshalWithOpts is identical to Unmarshal but also accepts a tftypes.UnmarshalOpts which contains
// options that can be used to modify the behaviour when unmarshalling JSON or Flatmap.
func (s RawState) UnmarshalWithOpts(typ tftypes.Type, opts UnmarshalOpts) (tftypes.Value, error) {
if s.JSON != nil {
return tftypes.ValueFromJSONWithOpts(s.JSON, typ, opts.ValueFromJSONOpts) //nolint:staticcheck
}
if s.Flatmap != nil {
return tftypes.Value{}, fmt.Errorf("flatmap states cannot be unmarshaled, only states written by Terraform 0.12 and higher can be unmarshaled")
}
return tftypes.Value{}, ErrUnknownRawStateType
}

View File

@ -743,6 +743,7 @@ func (s *server) ReadResource(ctx context.Context, req *tfplugin5.ReadResource_R
}
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "CurrentState", r.CurrentState)
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "ProviderMeta", r.ProviderMeta)
logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Request", "Private", r.Private)
ctx = tf5serverlogging.DownstreamRequest(ctx)
resp, err := s.downstream.ReadResource(ctx, r)
if err != nil {
@ -751,6 +752,7 @@ func (s *server) ReadResource(ctx context.Context, req *tfplugin5.ReadResource_R
}
tf5serverlogging.DownstreamResponse(ctx, resp.Diagnostics)
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Response", "NewState", resp.NewState)
logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Response", "Private", resp.Private)
ret, err := toproto.ReadResource_Response(resp)
if err != nil {
logging.ProtocolError(ctx, "Error converting response to protobuf", map[string]interface{}{logging.KeyError: err})
@ -776,6 +778,7 @@ func (s *server) PlanResourceChange(ctx context.Context, req *tfplugin5.PlanReso
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "PriorState", r.PriorState)
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "ProposedNewState", r.ProposedNewState)
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "ProviderMeta", r.ProviderMeta)
logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Request", "PriorPrivate", r.PriorPrivate)
ctx = tf5serverlogging.DownstreamRequest(ctx)
resp, err := s.downstream.PlanResourceChange(ctx, r)
if err != nil {
@ -784,6 +787,7 @@ func (s *server) PlanResourceChange(ctx context.Context, req *tfplugin5.PlanReso
}
tf5serverlogging.DownstreamResponse(ctx, resp.Diagnostics)
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Response", "PlannedState", resp.PlannedState)
logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Response", "PlannedPrivate", resp.PlannedPrivate)
ret, err := toproto.PlanResourceChange_Response(resp)
if err != nil {
logging.ProtocolError(ctx, "Error converting response to protobuf", map[string]interface{}{logging.KeyError: err})
@ -807,8 +811,9 @@ func (s *server) ApplyResourceChange(ctx context.Context, req *tfplugin5.ApplyRe
}
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "Config", r.Config)
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "PlannedState", r.PlannedState)
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "Config", r.Config)
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "Config", r.Config)
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "PriorState", r.PriorState)
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "ProviderMeta", r.ProviderMeta)
logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Request", "PlannedPrivate", r.PlannedPrivate)
ctx = tf5serverlogging.DownstreamRequest(ctx)
resp, err := s.downstream.ApplyResourceChange(ctx, r)
if err != nil {
@ -817,6 +822,7 @@ func (s *server) ApplyResourceChange(ctx context.Context, req *tfplugin5.ApplyRe
}
tf5serverlogging.DownstreamResponse(ctx, resp.Diagnostics)
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Response", "NewState", resp.NewState)
logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Response", "Private", resp.Private)
ret, err := toproto.ApplyResourceChange_Response(resp)
if err != nil {
logging.ProtocolError(ctx, "Error converting response to protobuf", map[string]interface{}{logging.KeyError: err})
@ -847,6 +853,7 @@ func (s *server) ImportResourceState(ctx context.Context, req *tfplugin5.ImportR
tf5serverlogging.DownstreamResponse(ctx, resp.Diagnostics)
for _, importedResource := range resp.ImportedResources {
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Response_ImportedResource", "State", importedResource.State)
logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Response_ImportedResource", "Private", importedResource.Private)
}
ret, err := toproto.ImportResourceState_Response(resp)
if err != nil {

View File

@ -31,10 +31,9 @@ func (d Diagnostics) ErrorCount() int {
// Log will log every diagnostic:
//
// - Error severity at ERROR level
// - Warning severity at WARN level
// - Invalid/Unknown severity at WARN level
//
// - Error severity at ERROR level
// - Warning severity at WARN level
// - Invalid/Unknown severity at WARN level
func (d Diagnostics) Log(ctx context.Context) {
for _, diagnostic := range d {
if diagnostic == nil {

View File

@ -21,10 +21,9 @@ func DownstreamRequest(ctx context.Context) context.Context {
// DownstreamResponse generates the following logging:
//
// - TRACE "Received downstream response" log with request duration and
// diagnostic severity counts
// - Per-diagnostic logs
//
// - TRACE "Received downstream response" log with request duration and
// diagnostic severity counts
// - Per-diagnostic logs
func DownstreamResponse(ctx context.Context, diagnostics diag.Diagnostics) {
responseFields := map[string]interface{}{
logging.KeyDiagnosticErrorCount: diagnostics.ErrorCount(),

View File

@ -19,7 +19,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc-gen-go v1.28.1
// protoc v3.19.4
// source: tfplugin6.proto
@ -1814,6 +1814,15 @@ func (x *ValidateProviderConfig_Response) GetDiagnostics() []*Diagnostic {
return nil
}
// Request is the message that is sent to the provider during the
// UpgradeResourceState RPC.
//
// This message intentionally does not include configuration data as any
// configuration-based or configuration-conditional changes should occur
// during the PlanResourceChange RPC. Additionally, the configuration is
// not guaranteed to exist (in the case of resource destruction), be wholly
// known, nor match the given prior state, which could lead to unexpected
// provider behaviors for practitioners.
type UpgradeResourceState_Request struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@ -2250,6 +2259,14 @@ func (x *ConfigureProvider_Response) GetDiagnostics() []*Diagnostic {
return nil
}
// Request is the message that is sent to the provider during the
// ReadResource RPC.
//
// This message intentionally does not include configuration data as any
// configuration-based or configuration-conditional changes should occur
// during the PlanResourceChange RPC. Additionally, the configuration is
// not guaranteed to be wholly known nor match the given prior state, which
// could lead to unexpected provider behaviors for practitioners.
type ReadResource_Request struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache

View File

@ -201,6 +201,15 @@ message ValidateProviderConfig {
}
message UpgradeResourceState {
// Request is the message that is sent to the provider during the
// UpgradeResourceState RPC.
//
// This message intentionally does not include configuration data as any
// configuration-based or configuration-conditional changes should occur
// during the PlanResourceChange RPC. Additionally, the configuration is
// not guaranteed to exist (in the case of resource destruction), be wholly
// known, nor match the given prior state, which could lead to unexpected
// provider behaviors for practitioners.
message Request {
string type_name = 1;
@ -258,6 +267,14 @@ message ConfigureProvider {
}
message ReadResource {
// Request is the message that is sent to the provider during the
// ReadResource RPC.
//
// This message intentionally does not include configuration data as any
// configuration-based or configuration-conditional changes should occur
// during the PlanResourceChange RPC. Additionally, the configuration is
// not guaranteed to be wholly known nor match the given prior state, which
// could lead to unexpected provider behaviors for practitioners.
message Request {
string type_name = 1;
DynamicValue current_state = 2;

View File

@ -54,7 +54,7 @@ type GetProviderSchemaResponse struct {
// will be specified in the provider block of the user's configuration.
Provider *Schema
// ProviderMeta defines the schema for the provider's metadta, which
// ProviderMeta defines the schema for the provider's metadata, which
// will be specified in the provider_meta blocks of the terraform block
// for a module. This is an advanced feature and its usage should be
// coordinated with the Terraform Core team by opening an issue at

View File

@ -77,3 +77,22 @@ func (s RawState) Unmarshal(typ tftypes.Type) (tftypes.Value, error) {
}
return tftypes.Value{}, ErrUnknownRawStateType
}
// UnmarshalOpts contains options that can be used to modify the behaviour when
// unmarshalling. Currently, this only contains a struct for opts for JSON but
// could have a field for Flatmap in the future.
type UnmarshalOpts struct {
ValueFromJSONOpts tftypes.ValueFromJSONOpts
}
// UnmarshalWithOpts is identical to Unmarshal but also accepts a tftypes.UnmarshalOpts which contains
// options that can be used to modify the behaviour when unmarshalling JSON or Flatmap.
func (s RawState) UnmarshalWithOpts(typ tftypes.Type, opts UnmarshalOpts) (tftypes.Value, error) {
if s.JSON != nil {
return tftypes.ValueFromJSONWithOpts(s.JSON, typ, opts.ValueFromJSONOpts) //nolint:staticcheck
}
if s.Flatmap != nil {
return tftypes.Value{}, fmt.Errorf("flatmap states cannot be unmarshaled, only states written by Terraform 0.12 and higher can be unmarshaled")
}
return tftypes.Value{}, ErrUnknownRawStateType
}

View File

@ -741,6 +741,7 @@ func (s *server) ReadResource(ctx context.Context, req *tfplugin6.ReadResource_R
}
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "CurrentState", r.CurrentState)
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "ProviderMeta", r.ProviderMeta)
logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Request", "Private", r.Private)
ctx = tf6serverlogging.DownstreamRequest(ctx)
resp, err := s.downstream.ReadResource(ctx, r)
if err != nil {
@ -749,6 +750,7 @@ func (s *server) ReadResource(ctx context.Context, req *tfplugin6.ReadResource_R
}
tf6serverlogging.DownstreamResponse(ctx, resp.Diagnostics)
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Response", "NewState", resp.NewState)
logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Response", "Private", resp.Private)
ret, err := toproto.ReadResource_Response(resp)
if err != nil {
logging.ProtocolError(ctx, "Error converting response to protobuf", map[string]interface{}{logging.KeyError: err})
@ -774,6 +776,7 @@ func (s *server) PlanResourceChange(ctx context.Context, req *tfplugin6.PlanReso
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "PriorState", r.PriorState)
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "ProposedNewState", r.ProposedNewState)
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "ProviderMeta", r.ProviderMeta)
logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Request", "PriorPrivate", r.PriorPrivate)
ctx = tf6serverlogging.DownstreamRequest(ctx)
resp, err := s.downstream.PlanResourceChange(ctx, r)
if err != nil {
@ -782,6 +785,7 @@ func (s *server) PlanResourceChange(ctx context.Context, req *tfplugin6.PlanReso
}
tf6serverlogging.DownstreamResponse(ctx, resp.Diagnostics)
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Response", "PlannedState", resp.PlannedState)
logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Response", "PlannedPrivate", resp.PlannedPrivate)
ret, err := toproto.PlanResourceChange_Response(resp)
if err != nil {
logging.ProtocolError(ctx, "Error converting response to protobuf", map[string]interface{}{logging.KeyError: err})
@ -805,8 +809,9 @@ func (s *server) ApplyResourceChange(ctx context.Context, req *tfplugin6.ApplyRe
}
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "Config", r.Config)
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "PlannedState", r.PlannedState)
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "Config", r.Config)
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "Config", r.Config)
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "PriorState", r.PriorState)
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Request", "ProviderMeta", r.ProviderMeta)
logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Request", "PlannedPrivate", r.PlannedPrivate)
ctx = tf6serverlogging.DownstreamRequest(ctx)
resp, err := s.downstream.ApplyResourceChange(ctx, r)
if err != nil {
@ -815,6 +820,7 @@ func (s *server) ApplyResourceChange(ctx context.Context, req *tfplugin6.ApplyRe
}
tf6serverlogging.DownstreamResponse(ctx, resp.Diagnostics)
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Response", "NewState", resp.NewState)
logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Response", "Private", resp.Private)
ret, err := toproto.ApplyResourceChange_Response(resp)
if err != nil {
logging.ProtocolError(ctx, "Error converting response to protobuf", map[string]interface{}{logging.KeyError: err})
@ -845,6 +851,7 @@ func (s *server) ImportResourceState(ctx context.Context, req *tfplugin6.ImportR
tf6serverlogging.DownstreamResponse(ctx, resp.Diagnostics)
for _, importedResource := range resp.ImportedResources {
logging.ProtocolData(ctx, s.protocolDataDir, rpc, "Response_ImportedResource", "State", importedResource.State)
logging.ProtocolPrivateData(ctx, s.protocolDataDir, rpc, "Response_ImportedResource", "Private", importedResource.Private)
}
ret, err := toproto.ImportResourceState_Response(resp)
if err != nil {

View File

@ -260,17 +260,13 @@ func (val Value) Copy() Value {
//
// The builtin Value representations are:
//
// * String: string, *string
//
// * Number: *big.Float, int64, *int64, int32, *int32, int16, *int16, int8,
// *int8, int, *int, uint64, *uint64, uint32, *uint32, uint16,
// *uint16, uint8, *uint8, uint, *uint, float64, *float64
//
// * Bool: bool, *bool
//
// * Map and Object: map[string]Value
//
// * Tuple, List, and Set: []Value
// - String: string, *string
// - Number: *big.Float, int64, *int64, int32, *int32, int16, *int16, int8,
// *int8, int, *int, uint64, *uint64, uint32, *uint32, uint16,
// *uint16, uint8, *uint8, uint, *uint, float64, *float64
// - Bool: bool, *bool
// - Map and Object: map[string]Value
// - Tuple, List, and Set: []Value
func NewValue(t Type, val interface{}) Value {
v, err := newValue(t, val)
if err != nil {

View File

@ -16,7 +16,24 @@ import (
// terraform-plugin-go. Third parties should not use it, and its behavior is
// not covered under the API compatibility guarantees. Don't use this.
func ValueFromJSON(data []byte, typ Type) (Value, error) {
return jsonUnmarshal(data, typ, NewAttributePath())
return jsonUnmarshal(data, typ, NewAttributePath(), ValueFromJSONOpts{})
}
// ValueFromJSONOpts contains options that can be used to modify the behaviour when
// unmarshalling JSON.
type ValueFromJSONOpts struct {
// IgnoreUndefinedAttributes is used to ignore any attributes which appear in the
// JSON but do not have a corresponding entry in the schema. For example, raw state
// where an attribute has been removed from the schema.
IgnoreUndefinedAttributes bool
}
// ValueFromJSONWithOpts is identical to ValueFromJSON with the exception that it
// accepts ValueFromJSONOpts which can be used to modify the unmarshalling behaviour, such
// as ignoring undefined attributes, for instance. This can occur when the JSON
// being unmarshalled does not have a corresponding attribute in the schema.
func ValueFromJSONWithOpts(data []byte, typ Type, opts ValueFromJSONOpts) (Value, error) {
return jsonUnmarshal(data, typ, NewAttributePath(), opts)
}
func jsonByteDecoder(buf []byte) *json.Decoder {
@ -26,7 +43,7 @@ func jsonByteDecoder(buf []byte) *json.Decoder {
return dec
}
func jsonUnmarshal(buf []byte, typ Type, p *AttributePath) (Value, error) {
func jsonUnmarshal(buf []byte, typ Type, p *AttributePath, opts ValueFromJSONOpts) (Value, error) {
dec := jsonByteDecoder(buf)
tok, err := dec.Token()
@ -46,18 +63,17 @@ func jsonUnmarshal(buf []byte, typ Type, p *AttributePath) (Value, error) {
case typ.Is(Bool):
return jsonUnmarshalBool(buf, typ, p)
case typ.Is(DynamicPseudoType):
return jsonUnmarshalDynamicPseudoType(buf, typ, p)
return jsonUnmarshalDynamicPseudoType(buf, typ, p, opts)
case typ.Is(List{}):
return jsonUnmarshalList(buf, typ.(List).ElementType, p)
return jsonUnmarshalList(buf, typ.(List).ElementType, p, opts)
case typ.Is(Set{}):
return jsonUnmarshalSet(buf, typ.(Set).ElementType, p)
return jsonUnmarshalSet(buf, typ.(Set).ElementType, p, opts)
case typ.Is(Map{}):
return jsonUnmarshalMap(buf, typ.(Map).ElementType, p)
return jsonUnmarshalMap(buf, typ.(Map).ElementType, p, opts)
case typ.Is(Tuple{}):
return jsonUnmarshalTuple(buf, typ.(Tuple).ElementTypes, p)
return jsonUnmarshalTuple(buf, typ.(Tuple).ElementTypes, p, opts)
case typ.Is(Object{}):
return jsonUnmarshalObject(buf, typ.(Object).AttributeTypes, p)
return jsonUnmarshalObject(buf, typ.(Object).AttributeTypes, p, opts)
}
return Value{}, p.NewErrorf("unknown type %s", typ)
}
@ -140,7 +156,7 @@ func jsonUnmarshalBool(buf []byte, _ Type, p *AttributePath) (Value, error) {
return Value{}, p.NewErrorf("unsupported type %T sent as %s", tok, Bool)
}
func jsonUnmarshalDynamicPseudoType(buf []byte, _ Type, p *AttributePath) (Value, error) {
func jsonUnmarshalDynamicPseudoType(buf []byte, _ Type, p *AttributePath, opts ValueFromJSONOpts) (Value, error) {
dec := jsonByteDecoder(buf)
tok, err := dec.Token()
if err != nil {
@ -190,10 +206,10 @@ func jsonUnmarshalDynamicPseudoType(buf []byte, _ Type, p *AttributePath) (Value
if valBody == nil {
return Value{}, p.NewErrorf("missing value in dynamically-typed value")
}
return jsonUnmarshal(valBody, t, p)
return jsonUnmarshal(valBody, t, p, opts)
}
func jsonUnmarshalList(buf []byte, elementType Type, p *AttributePath) (Value, error) {
func jsonUnmarshalList(buf []byte, elementType Type, p *AttributePath, opts ValueFromJSONOpts) (Value, error) {
dec := jsonByteDecoder(buf)
tok, err := dec.Token()
@ -227,7 +243,7 @@ func jsonUnmarshalList(buf []byte, elementType Type, p *AttributePath) (Value, e
if err != nil {
return Value{}, innerPath.NewErrorf("error decoding value: %w", err)
}
val, err := jsonUnmarshal(rawVal, elementType, innerPath)
val, err := jsonUnmarshal(rawVal, elementType, innerPath, opts)
if err != nil {
return Value{}, err
}
@ -254,7 +270,7 @@ func jsonUnmarshalList(buf []byte, elementType Type, p *AttributePath) (Value, e
}, vals), nil
}
func jsonUnmarshalSet(buf []byte, elementType Type, p *AttributePath) (Value, error) {
func jsonUnmarshalSet(buf []byte, elementType Type, p *AttributePath, opts ValueFromJSONOpts) (Value, error) {
dec := jsonByteDecoder(buf)
tok, err := dec.Token()
@ -284,7 +300,7 @@ func jsonUnmarshalSet(buf []byte, elementType Type, p *AttributePath) (Value, er
if err != nil {
return Value{}, innerPath.NewErrorf("error decoding value: %w", err)
}
val, err := jsonUnmarshal(rawVal, elementType, innerPath)
val, err := jsonUnmarshal(rawVal, elementType, innerPath, opts)
if err != nil {
return Value{}, err
}
@ -310,7 +326,7 @@ func jsonUnmarshalSet(buf []byte, elementType Type, p *AttributePath) (Value, er
}, vals), nil
}
func jsonUnmarshalMap(buf []byte, attrType Type, p *AttributePath) (Value, error) {
func jsonUnmarshalMap(buf []byte, attrType Type, p *AttributePath, opts ValueFromJSONOpts) (Value, error) {
dec := jsonByteDecoder(buf)
tok, err := dec.Token()
@ -341,7 +357,7 @@ func jsonUnmarshalMap(buf []byte, attrType Type, p *AttributePath) (Value, error
if err != nil {
return Value{}, innerPath.NewErrorf("error decoding value: %w", err)
}
val, err := jsonUnmarshal(rawVal, attrType, innerPath)
val, err := jsonUnmarshal(rawVal, attrType, innerPath, opts)
if err != nil {
return Value{}, err
}
@ -360,7 +376,7 @@ func jsonUnmarshalMap(buf []byte, attrType Type, p *AttributePath) (Value, error
}, vals), nil
}
func jsonUnmarshalTuple(buf []byte, elementTypes []Type, p *AttributePath) (Value, error) {
func jsonUnmarshalTuple(buf []byte, elementTypes []Type, p *AttributePath, opts ValueFromJSONOpts) (Value, error) {
dec := jsonByteDecoder(buf)
tok, err := dec.Token()
@ -398,7 +414,7 @@ func jsonUnmarshalTuple(buf []byte, elementTypes []Type, p *AttributePath) (Valu
if err != nil {
return Value{}, innerPath.NewErrorf("error decoding value: %w", err)
}
val, err := jsonUnmarshal(rawVal, elementType, innerPath)
val, err := jsonUnmarshal(rawVal, elementType, innerPath, opts)
if err != nil {
return Value{}, err
}
@ -422,7 +438,9 @@ func jsonUnmarshalTuple(buf []byte, elementTypes []Type, p *AttributePath) (Valu
}, vals), nil
}
func jsonUnmarshalObject(buf []byte, attrTypes map[string]Type, p *AttributePath) (Value, error) {
// jsonUnmarshalObject attempts to decode JSON object structure to tftypes.Value object.
// opts contains fields that can be used to modify the behaviour of JSON unmarshalling.
func jsonUnmarshalObject(buf []byte, attrTypes map[string]Type, p *AttributePath, opts ValueFromJSONOpts) (Value, error) {
dec := jsonByteDecoder(buf)
tok, err := dec.Token()
@ -435,27 +453,32 @@ func jsonUnmarshalObject(buf []byte, attrTypes map[string]Type, p *AttributePath
vals := map[string]Value{}
for dec.More() {
innerPath := p.WithElementKeyValue(NewValue(String, UnknownValue))
tok, err := dec.Token()
if err != nil {
return Value{}, innerPath.NewErrorf("error reading token: %w", err)
return Value{}, p.NewErrorf("error reading object attribute key token: %w", err)
}
key, ok := tok.(string)
if !ok {
return Value{}, innerPath.NewErrorf("object attribute key was %T, not string", tok)
return Value{}, p.NewErrorf("object attribute key was %T with value %v, not string", tok, tok)
}
innerPath := p.WithAttributeName(key)
attrType, ok := attrTypes[key]
if !ok {
if opts.IgnoreUndefinedAttributes {
// We are trying to ignore the key and value of any unsupported attribute.
_ = dec.Decode(new(json.RawMessage))
continue
}
return Value{}, innerPath.NewErrorf("unsupported attribute %q", key)
}
innerPath = p.WithAttributeName(key)
var rawVal json.RawMessage
err = dec.Decode(&rawVal)
if err != nil {
return Value{}, innerPath.NewErrorf("error decoding value: %w", err)
}
val, err := jsonUnmarshal(rawVal, attrType, innerPath)
val, err := jsonUnmarshal(rawVal, attrType, innerPath, opts)
if err != nil {
return Value{}, err
}

View File

@ -1,3 +1,5 @@
Copyright (c) 2019 HashiCorp, Inc.
Mozilla Public License, version 2.0
1. Definitions

View File

@ -6,9 +6,9 @@ import "fmt"
// as the most common use case in Go will be handling a single error
// returned from a function.
//
// if err != nil {
// return diag.FromErr(err)
// }
// if err != nil {
// return diag.FromErr(err)
// }
func FromErr(err error) Diagnostics {
if err == nil {
return nil
@ -26,9 +26,9 @@ func FromErr(err error) Diagnostics {
// values. This returns a single error in a Diagnostics as errors typically
// do not occur in multiples as warnings may.
//
// if unexpectedCondition {
// return diag.Errorf("unexpected: %s", someValue)
// }
// if unexpectedCondition {
// return diag.Errorf("unexpected: %s", someValue)
// }
func Errorf(format string, a ...interface{}) Diagnostics {
return Diagnostics{
Diagnostic{

View File

@ -3,7 +3,6 @@ package logging
import (
"fmt"
"io"
"io/ioutil"
"log"
"os"
"strings"
@ -32,7 +31,7 @@ var ValidLevels = []logutils.LogLevel{"TRACE", "DEBUG", "INFO", "WARN", "ERROR"}
// environment variable. Calls to tflog.* will have their output managed by the
// tfsdklog sink.
func LogOutput(t testing.T) (logOutput io.Writer, err error) {
logOutput = ioutil.Discard
logOutput = io.Discard
logLevel := LogLevel()
if logLevel == "" {
@ -88,7 +87,7 @@ func LogOutput(t testing.T) (logOutput io.Writer, err error) {
// SetOutput checks for a log destination with LogOutput, and calls
// log.SetOutput with the result. If LogOutput returns nil, SetOutput uses
// ioutil.Discard. Any error from LogOutout is fatal.
// io.Discard. Any error from LogOutout is fatal.
func SetOutput(t testing.T) {
out, err := LogOutput(t)
if err != nil {
@ -96,7 +95,7 @@ func SetOutput(t testing.T) {
}
if out == nil {
out = ioutil.Discard
out = io.Discard
}
log.SetOutput(out)

View File

@ -3,7 +3,7 @@ package resource
import (
"context"
"fmt"
"io/ioutil"
"io"
"os"
"strings"
"sync"
@ -157,6 +157,13 @@ func runProviderCommand(ctx context.Context, t testing.T, f func() error, wd *pl
host = v
}
// schema.Provider have a global stop context that is created outside
// the server context and have their own associated goroutine. Since
// Terraform does not call the StopProvider RPC to stop the server in
// reattach mode, ensure that we save these servers to later call that
// RPC and end those goroutines.
legacyProviderServers := make([]*schema.GRPCProviderServer, 0, len(factories.legacy))
// Spin up gRPC servers for every provider factory, start a
// WaitGroup to listen for all of the close channels.
var wg sync.WaitGroup
@ -180,18 +187,24 @@ func runProviderCommand(ctx context.Context, t testing.T, f func() error, wd *pl
// shut down.
wg.Add(1)
grpcProviderServer := schema.NewGRPCProviderServer(provider)
legacyProviderServers = append(legacyProviderServers, grpcProviderServer)
// Ensure StopProvider is always called when returning early.
defer grpcProviderServer.StopProvider(ctx, nil) //nolint:errcheck // does not return errors
// configure the settings our plugin will be served with
// the GRPCProviderFunc wraps a non-gRPC provider server
// into a gRPC interface, and the logger just discards logs
// from go-plugin.
opts := &plugin.ServeOpts{
GRPCProviderFunc: func() tfprotov5.ProviderServer {
return schema.NewGRPCProviderServer(provider)
return grpcProviderServer
},
Logger: hclog.New(&hclog.LoggerOptions{
Name: "plugintest",
Level: hclog.Trace,
Output: ioutil.Discard,
Output: io.Discard,
}),
NoLogOutputOverride: true,
UseTFLogSink: t,
@ -279,7 +292,7 @@ func runProviderCommand(ctx context.Context, t testing.T, f func() error, wd *pl
Logger: hclog.New(&hclog.LoggerOptions{
Name: "plugintest",
Level: hclog.Trace,
Output: ioutil.Discard,
Output: io.Discard,
}),
NoLogOutputOverride: true,
UseTFLogSink: t,
@ -364,7 +377,7 @@ func runProviderCommand(ctx context.Context, t testing.T, f func() error, wd *pl
Logger: hclog.New(&hclog.LoggerOptions{
Name: "plugintest",
Level: hclog.Trace,
Output: ioutil.Discard,
Output: io.Discard,
}),
NoLogOutputOverride: true,
UseTFLogSink: t,
@ -430,6 +443,12 @@ func runProviderCommand(ctx context.Context, t testing.T, f func() error, wd *pl
// get closed, and we'll hang here.
cancel()
// For legacy providers, call the StopProvider RPC so the StopContext
// goroutine is cleaned up properly.
for _, legacyProviderServer := range legacyProviderServers {
legacyProviderServer.StopProvider(ctx, nil) //nolint:errcheck // does not return errors
}
logging.HelperResourceTrace(ctx, "Waiting for providers to stop")
// wait for the servers to actually shut down; it may take a moment for

View File

@ -9,7 +9,7 @@ import (
// providerConfig takes the list of providers in a TestCase and returns a
// config with only empty provider blocks. This is useful for Import, where no
// config is provided, but the providers must be defined.
func (c TestCase) providerConfig(_ context.Context) string {
func (c TestCase) providerConfig(_ context.Context, skipProviderBlock bool) string {
var providerBlocks, requiredProviderBlocks strings.Builder
// [BF] The Providers field handling predates the logic being moved to this
@ -21,7 +21,9 @@ func (c TestCase) providerConfig(_ context.Context) string {
}
for name, externalProvider := range c.ExternalProviders {
providerBlocks.WriteString(fmt.Sprintf("provider %q {}\n", name))
if !skipProviderBlock {
providerBlocks.WriteString(fmt.Sprintf("provider %q {}\n", name))
}
if externalProvider.Source == "" && externalProvider.VersionConstraint == "" {
continue

View File

@ -36,10 +36,9 @@ func (c TestCase) hasProviders(_ context.Context) bool {
// validate ensures the TestCase is valid based on the following criteria:
//
// - No overlapping ExternalProviders and Providers entries
// - No overlapping ExternalProviders and ProviderFactories entries
// - TestStep validations performed by the (TestStep).validate() method.
//
// - No overlapping ExternalProviders and Providers entries
// - No overlapping ExternalProviders and ProviderFactories entries
// - TestStep validations performed by the (TestStep).validate() method.
func (c TestCase) validate(ctx context.Context) error {
logging.HelperResourceTrace(ctx, "Validating TestCase")

View File

@ -100,10 +100,10 @@ func AddTestSweepers(name string, s *Sweeper) {
//
// Sweeper flags added to the "go test" command:
//
// -sweep: Comma-separated list of locations/regions to run available sweepers.
// -sweep-allow-failues: Enable to allow other sweepers to run after failures.
// -sweep-run: Comma-separated list of resource type sweepers to run. Defaults
// to all sweepers.
// -sweep: Comma-separated list of locations/regions to run available sweepers.
// -sweep-allow-failues: Enable to allow other sweepers to run after failures.
// -sweep-run: Comma-separated list of resource type sweepers to run. Defaults
// to all sweepers.
//
// Refer to the Env prefixed constants for environment variables that further
// control testing functionality.
@ -551,6 +551,16 @@ type TestStep struct {
// ImportStateCheck checks the results of ImportState. It should be
// used to verify that the resulting value of ImportState has the
// proper resources, IDs, and attributes.
//
// Prefer ImportStateVerify over ImportStateCheck, unless the resource
// import explicitly is expected to create multiple resources (not a
// recommended resource implementation) or if attributes are imported with
// syntactically different but semantically/functionally equivalent values
// where special logic is needed.
//
// Terraform versions 1.3 and later can include data source states during
// import, which the testing framework will skip to prevent the need for
// Terraform version specific logic in provider testing.
ImportStateCheck ImportStateCheckFunc
// ImportStateVerify, if true, will also check that the state values
@ -564,6 +574,28 @@ type TestStep struct {
ImportStateVerify bool
ImportStateVerifyIgnore []string
// ImportStatePersist, if true, will update the persisted state with the
// state generated by the import operation (i.e., terraform import). When
// false (default) the state generated by the import operation is discarded
// at the end of the test step that is verifying import behavior.
ImportStatePersist bool
//---------------------------------------------------------------
// RefreshState testing
//---------------------------------------------------------------
// RefreshState, if true, will test the functionality of `terraform
// refresh` by refreshing the state, running any checks against the
// refreshed state, and running a plan to verify against unexpected plan
// differences.
//
// If the refresh is expected to result in a non-empty plan
// ExpectNonEmptyPlan should be set to true in the same TestStep.
//
// RefreshState cannot be the first TestStep and, it is mutually exclusive
// with ImportState.
RefreshState bool
// ProviderFactories can be specified for the providers that are valid for
// this TestStep. When providers are specified at the TestStep level, all
// TestStep within a TestCase must declare providers.
@ -665,16 +697,16 @@ func ParallelTest(t testing.T, c TestCase) {
// This function will automatically find or install Terraform CLI into a
// temporary directory, based on the following behavior:
//
// - If the TF_ACC_TERRAFORM_PATH environment variable is set, that
// Terraform CLI binary is used if found and executable. If not found or
// executable, an error will be returned unless the
// TF_ACC_TERRAFORM_VERSION environment variable is also set.
// - If the TF_ACC_TERRAFORM_VERSION environment variable is set, install
// and use that Terraform CLI version.
// - If both the TF_ACC_TERRAFORM_PATH and TF_ACC_TERRAFORM_VERSION
// environment variables are unset, perform a lookup for the Terraform
// CLI binary based on the operating system PATH. If not found, the
// latest available Terraform CLI binary is installed.
// - If the TF_ACC_TERRAFORM_PATH environment variable is set, that
// Terraform CLI binary is used if found and executable. If not found or
// executable, an error will be returned unless the
// TF_ACC_TERRAFORM_VERSION environment variable is also set.
// - If the TF_ACC_TERRAFORM_VERSION environment variable is set, install
// and use that Terraform CLI version.
// - If both the TF_ACC_TERRAFORM_PATH and TF_ACC_TERRAFORM_VERSION
// environment variables are unset, perform a lookup for the Terraform
// CLI binary based on the operating system PATH. If not found, the
// latest available Terraform CLI binary is installed.
//
// Refer to the Env prefixed constants for additional details about these
// environment variables, and others, that control testing functionality.
@ -821,35 +853,35 @@ func ComposeAggregateTestCheckFunc(fs ...TestCheckFunc) TestCheckFunc {
// Use this as a last resort when a more specific TestCheckFunc cannot be
// implemented, such as:
//
// - TestCheckResourceAttr: Equality checking of non-TypeSet state value.
// - TestCheckResourceAttrPair: Equality checking of non-TypeSet state
// value, based on another state value.
// - TestCheckTypeSet*: Equality checking of TypeSet state values.
// - TestMatchResourceAttr: Regular expression checking of non-TypeSet
// state value.
// - TestMatchTypeSet*: Regular expression checking on TypeSet state values.
// - TestCheckResourceAttr: Equality checking of non-TypeSet state value.
// - TestCheckResourceAttrPair: Equality checking of non-TypeSet state
// value, based on another state value.
// - TestCheckTypeSet*: Equality checking of TypeSet state values.
// - TestMatchResourceAttr: Regular expression checking of non-TypeSet
// state value.
// - TestMatchTypeSet*: Regular expression checking on TypeSet state values.
//
// For managed resources, the name parameter is combination of the resource
// type, a period (.), and the name label. The name for the below example
// configuration would be "myprovider_thing.example".
//
// resource "myprovider_thing" "example" { ... }
// resource "myprovider_thing" "example" { ... }
//
// For data sources, the name parameter is a combination of the keyword "data",
// a period (.), the data source type, a period (.), and the name label. The
// name for the below example configuration would be
// "data.myprovider_thing.example".
//
// data "myprovider_thing" "example" { ... }
// data "myprovider_thing" "example" { ... }
//
// The key parameter is an attribute path in Terraform CLI 0.11 and earlier
// "flatmap" syntax. Keys start with the attribute name of a top-level
// attribute. Use the following special key syntax to inspect underlying
// values of a list or map attribute:
//
// - .{NUMBER}: List value at index, e.g. .0 to inspect the first element
// - .{KEY}: Map value at key, e.g. .example to inspect the example key
// value
// - .{NUMBER}: List value at index, e.g. .0 to inspect the first element
// - .{KEY}: Map value at key, e.g. .example to inspect the example key
// value
//
// While it is possible to check nested attributes under list and map
// attributes using the special key syntax, checking a list, map, or set
@ -918,34 +950,34 @@ func testCheckResourceAttrSet(is *terraform.InstanceState, name string, key stri
// type, a period (.), and the name label. The name for the below example
// configuration would be "myprovider_thing.example".
//
// resource "myprovider_thing" "example" { ... }
// resource "myprovider_thing" "example" { ... }
//
// For data sources, the name parameter is a combination of the keyword "data",
// a period (.), the data source type, a period (.), and the name label. The
// name for the below example configuration would be
// "data.myprovider_thing.example".
//
// data "myprovider_thing" "example" { ... }
// data "myprovider_thing" "example" { ... }
//
// The key parameter is an attribute path in Terraform CLI 0.11 and earlier
// "flatmap" syntax. Keys start with the attribute name of a top-level
// attribute. Use the following special key syntax to inspect list, map, and
// set attributes:
//
// - .{NUMBER}: List value at index, e.g. .0 to inspect the first element.
// Use the TestCheckTypeSet* and TestMatchTypeSet* functions instead
// for sets.
// - .{KEY}: Map value at key, e.g. .example to inspect the example key
// value.
// - .#: Number of elements in list or set.
// - .%: Number of elements in map.
// - .{NUMBER}: List value at index, e.g. .0 to inspect the first element.
// Use the TestCheckTypeSet* and TestMatchTypeSet* functions instead
// for sets.
// - .{KEY}: Map value at key, e.g. .example to inspect the example key
// value.
// - .#: Number of elements in list or set.
// - .%: Number of elements in map.
//
// The value parameter is the stringified data to check at the given key. Use
// the following attribute type rules to set the value:
//
// - Boolean: "false" or "true".
// - Float/Integer: Stringified number, such as "1.2" or "123".
// - String: No conversion necessary.
// - Boolean: "false" or "true".
// - Float/Integer: Stringified number, such as "1.2" or "123".
// - String: No conversion necessary.
func TestCheckResourceAttr(name, key, value string) TestCheckFunc {
return checkIfIndexesIntoTypeSet(key, func(s *terraform.State) error {
is, err := primaryInstanceState(s, name)
@ -1032,27 +1064,27 @@ type CheckResourceAttrWithFunc func(value string) error
// type, a period (.), and the name label. The name for the below example
// configuration would be "myprovider_thing.example".
//
// resource "myprovider_thing" "example" { ... }
// resource "myprovider_thing" "example" { ... }
//
// For data sources, the name parameter is a combination of the keyword "data",
// a period (.), the data source type, a period (.), and the name label. The
// name for the below example configuration would be
// "data.myprovider_thing.example".
//
// data "myprovider_thing" "example" { ... }
// data "myprovider_thing" "example" { ... }
//
// The key parameter is an attribute path in Terraform CLI 0.11 and earlier
// "flatmap" syntax. Keys start with the attribute name of a top-level
// attribute. Use the following special key syntax to inspect list, map, and
// set attributes:
//
// - .{NUMBER}: List value at index, e.g. .0 to inspect the first element.
// Use the TestCheckTypeSet* and TestMatchTypeSet* functions instead
// for sets.
// - .{KEY}: Map value at key, e.g. .example to inspect the example key
// value.
// - .#: Number of elements in list or set.
// - .%: Number of elements in map.
// - .{NUMBER}: List value at index, e.g. .0 to inspect the first element.
// Use the TestCheckTypeSet* and TestMatchTypeSet* functions instead
// for sets.
// - .{KEY}: Map value at key, e.g. .example to inspect the example key
// value.
// - .#: Number of elements in list or set.
// - .%: Number of elements in map.
//
// The checkValueFunc parameter is a CheckResourceAttrWithFunc,
// and it's provided with the attribute value to apply a custom checking logic,
@ -1088,23 +1120,23 @@ func TestCheckResourceAttrWith(name, key string, checkValueFunc CheckResourceAtt
// type, a period (.), and the name label. The name for the below example
// configuration would be "myprovider_thing.example".
//
// resource "myprovider_thing" "example" { ... }
// resource "myprovider_thing" "example" { ... }
//
// For data sources, the name parameter is a combination of the keyword "data",
// a period (.), the data source type, a period (.), and the name label. The
// name for the below example configuration would be
// "data.myprovider_thing.example".
//
// data "myprovider_thing" "example" { ... }
// data "myprovider_thing" "example" { ... }
//
// The key parameter is an attribute path in Terraform CLI 0.11 and earlier
// "flatmap" syntax. Keys start with the attribute name of a top-level
// attribute. Use the following special key syntax to inspect underlying
// values of a list or map attribute:
//
// - .{NUMBER}: List value at index, e.g. .0 to inspect the first element.
// - .{KEY}: Map value at key, e.g. .example to inspect the example key
// value.
// - .{NUMBER}: List value at index, e.g. .0 to inspect the first element.
// - .{KEY}: Map value at key, e.g. .example to inspect the example key
// value.
//
// While it is possible to check nested attributes under list and map
// attributes using the special key syntax, checking a list, map, or set
@ -1180,27 +1212,27 @@ func testCheckNoResourceAttr(is *terraform.InstanceState, name string, key strin
// type, a period (.), and the name label. The name for the below example
// configuration would be "myprovider_thing.example".
//
// resource "myprovider_thing" "example" { ... }
// resource "myprovider_thing" "example" { ... }
//
// For data sources, the name parameter is a combination of the keyword "data",
// a period (.), the data source type, a period (.), and the name label. The
// name for the below example configuration would be
// "data.myprovider_thing.example".
//
// data "myprovider_thing" "example" { ... }
// data "myprovider_thing" "example" { ... }
//
// The key parameter is an attribute path in Terraform CLI 0.11 and earlier
// "flatmap" syntax. Keys start with the attribute name of a top-level
// attribute. Use the following special key syntax to inspect list, map, and
// set attributes:
//
// - .{NUMBER}: List value at index, e.g. .0 to inspect the first element.
// Use the TestCheckTypeSet* and TestMatchTypeSet* functions instead
// for sets.
// - .{KEY}: Map value at key, e.g. .example to inspect the example key
// value.
// - .#: Number of elements in list or set.
// - .%: Number of elements in map.
// - .{NUMBER}: List value at index, e.g. .0 to inspect the first element.
// Use the TestCheckTypeSet* and TestMatchTypeSet* functions instead
// for sets.
// - .{KEY}: Map value at key, e.g. .example to inspect the example key
// value.
// - .#: Number of elements in list or set.
// - .%: Number of elements in map.
//
// The value parameter is a compiled regular expression. A typical pattern is
// using the regexp.MustCompile() function, which will automatically ensure the
@ -1273,14 +1305,14 @@ func TestCheckModuleResourceAttrPtr(mp []string, name string, key string, value
// type, a period (.), and the name label. The name for the below example
// configuration would be "myprovider_thing.example".
//
// resource "myprovider_thing" "example" { ... }
// resource "myprovider_thing" "example" { ... }
//
// For data sources, the name parameter is a combination of the keyword "data",
// a period (.), the data source type, a period (.), and the name label. The
// name for the below example configuration would be
// "data.myprovider_thing.example".
//
// data "myprovider_thing" "example" { ... }
// data "myprovider_thing" "example" { ... }
//
// The first and second names may use any combination of managed resources
// and/or data sources.
@ -1290,13 +1322,13 @@ func TestCheckModuleResourceAttrPtr(mp []string, name string, key string, value
// attribute. Use the following special key syntax to inspect list, map, and
// set attributes:
//
// - .{NUMBER}: List value at index, e.g. .0 to inspect the first element.
// Use the TestCheckTypeSet* and TestMatchTypeSet* functions instead
// for sets.
// - .{KEY}: Map value at key, e.g. .example to inspect the example key
// value.
// - .#: Number of elements in list or set.
// - .%: Number of elements in map.
// - .{NUMBER}: List value at index, e.g. .0 to inspect the first element.
// Use the TestCheckTypeSet* and TestMatchTypeSet* functions instead
// for sets.
// - .{KEY}: Map value at key, e.g. .example to inspect the example key
// value.
// - .#: Number of elements in list or set.
// - .%: Number of elements in map.
func TestCheckResourceAttrPair(nameFirst, keyFirst, nameSecond, keySecond string) TestCheckFunc {
return checkIfIndexesIntoTypeSetPair(keyFirst, keySecond, func(s *terraform.State) error {
isFirst, err := primaryInstanceState(s, nameFirst)

View File

@ -2,14 +2,13 @@ package resource
import (
"context"
"errors"
"fmt"
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/logging"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/plugintest"
)
func testStepTaint(ctx context.Context, state *terraform.State, step TestStep) error {
func testStepTaint(ctx context.Context, step TestStep, wd *plugintest.WorkingDir) error {
if len(step.Taint) == 0 {
return nil
}
@ -17,16 +16,10 @@ func testStepTaint(ctx context.Context, state *terraform.State, step TestStep) e
logging.HelperResourceTrace(ctx, fmt.Sprintf("Using TestStep Taint: %v", step.Taint))
for _, p := range step.Taint {
m := state.RootModule()
if m == nil {
return errors.New("no state")
err := wd.Taint(ctx, p)
if err != nil {
return fmt.Errorf("error tainting resource: %s", err)
}
rs, ok := m.Resources[p]
if !ok {
return fmt.Errorf("resource %q not found in state", p)
}
logging.HelperResourceWarn(ctx, fmt.Sprintf("Explicitly tainting resource %q", p))
rs.Taint()
}
return nil
}

View File

@ -8,7 +8,7 @@ import (
"github.com/davecgh/go-spew/spew"
tfjson "github.com/hashicorp/terraform-json"
testing "github.com/mitchellh/go-testing-interface"
"github.com/mitchellh/go-testing-interface"
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/logging"
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/plugintest"
@ -89,7 +89,7 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest
}()
if c.hasProviders(ctx) {
err := wd.SetConfig(ctx, c.providerConfig(ctx))
err := wd.SetConfig(ctx, c.providerConfig(ctx, false))
if err != nil {
logging.HelperResourceError(ctx,
@ -114,7 +114,7 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest
logging.HelperResourceDebug(ctx, "Starting TestSteps")
// use this to track last step succesfully applied
// use this to track last step successfully applied
// acts as default for import tests
var appliedCfg string
@ -152,29 +152,7 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest
}
if step.Config != "" && !step.Destroy && len(step.Taint) > 0 {
var state *terraform.State
err := runProviderCommand(ctx, t, func() error {
var err error
state, err = getState(ctx, t, wd)
if err != nil {
return err
}
return nil
}, wd, providers)
if err != nil {
logging.HelperResourceError(ctx,
"TestStep error reading prior state before tainting resources",
map[string]interface{}{logging.KeyError: err},
)
t.Fatalf("TestStep %d/%d error reading prior state before tainting resources: %s", stepNumber, len(c.Steps), err)
}
err = testStepTaint(ctx, state, step)
err := testStepTaint(ctx, step, wd)
if err != nil {
logging.HelperResourceError(ctx,
@ -192,7 +170,7 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest
protov6: protov6ProviderFactories(c.ProtoV6ProviderFactories).merge(step.ProtoV6ProviderFactories),
}
providerCfg := step.providerConfig(ctx)
providerCfg := step.providerConfig(ctx, step.configHasProviderBlock(ctx))
err := wd.SetConfig(ctx, providerCfg)
@ -263,6 +241,45 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest
continue
}
if step.RefreshState {
logging.HelperResourceTrace(ctx, "TestStep is RefreshState mode")
err := testStepNewRefreshState(ctx, t, wd, step, providers)
if step.ExpectError != nil {
logging.HelperResourceDebug(ctx, "Checking TestStep ExpectError")
if err == nil {
logging.HelperResourceError(ctx,
"Error running refresh: expected an error but got none",
)
t.Fatalf("Step %d/%d error running refresh: expected an error but got none", stepNumber, len(c.Steps))
}
if !step.ExpectError.MatchString(err.Error()) {
logging.HelperResourceError(ctx,
fmt.Sprintf("Error running refresh: expected an error with pattern (%s)", step.ExpectError.String()),
map[string]interface{}{logging.KeyError: err},
)
t.Fatalf("Step %d/%d error running refresh, expected an error with pattern (%s), no match on: %s", stepNumber, len(c.Steps), step.ExpectError.String(), err)
}
} else {
if err != nil && c.ErrorCheck != nil {
logging.HelperResourceDebug(ctx, "Calling TestCase ErrorCheck")
err = c.ErrorCheck(err)
logging.HelperResourceDebug(ctx, "Called TestCase ErrorCheck")
}
if err != nil {
logging.HelperResourceError(ctx,
"Error running refresh",
map[string]interface{}{logging.KeyError: err},
)
t.Fatalf("Step %d/%d error running refresh: %s", stepNumber, len(c.Steps), err)
}
}
logging.HelperResourceDebug(ctx, "Finished TestStep")
continue
}
if step.Config != "" {
logging.HelperResourceTrace(ctx, "TestStep is Config mode")
@ -300,7 +317,7 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest
}
}
appliedCfg = step.Config
appliedCfg = step.mergedConfig(ctx, c)
logging.HelperResourceDebug(ctx, "Finished TestStep")
@ -354,7 +371,7 @@ func testIDRefresh(ctx context.Context, t testing.T, c TestCase, wd *plugintest.
// Temporarily set the config to a minimal provider config for the refresh
// test. After the refresh we can reset it.
err := wd.SetConfig(ctx, c.providerConfig(ctx))
err := wd.SetConfig(ctx, c.providerConfig(ctx, step.configHasProviderBlock(ctx)))
if err != nil {
t.Fatalf("Error setting import test config: %s", err)
}

View File

@ -16,7 +16,7 @@ import (
func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugintest.WorkingDir, step TestStep, providers *providerFactories) error {
t.Helper()
err := wd.SetConfig(ctx, step.Config)
err := wd.SetConfig(ctx, step.mergedConfig(ctx, c))
if err != nil {
return fmt.Errorf("Error setting config: %w", err)
}

View File

@ -7,7 +7,7 @@ import (
"strings"
"github.com/davecgh/go-spew/spew"
testing "github.com/mitchellh/go-testing-interface"
"github.com/mitchellh/go-testing-interface"
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/logging"
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/plugintest"
@ -86,8 +86,17 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest
t.Fatal("Cannot import state with no specified config")
}
}
importWd := helper.RequireNewWorkingDir(ctx, t)
defer importWd.Close()
var importWd *plugintest.WorkingDir
// Use the same working directory to persist the state from import
if step.ImportStatePersist {
importWd = wd
} else {
importWd = helper.RequireNewWorkingDir(ctx, t)
defer importWd.Close()
}
err = importWd.SetConfig(ctx, step.Config)
if err != nil {
t.Fatalf("Error setting test config: %s", err)
@ -95,11 +104,13 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest
logging.HelperResourceDebug(ctx, "Running Terraform CLI init and import")
err = runProviderCommand(ctx, t, func() error {
return importWd.Init(ctx)
}, importWd, providers)
if err != nil {
t.Fatalf("Error running init: %s", err)
if !step.ImportStatePersist {
err = runProviderCommand(ctx, t, func() error {
return importWd.Init(ctx)
}, importWd, providers)
if err != nil {
t.Fatalf("Error running init: %s", err)
}
}
err = runProviderCommand(ctx, t, func() error {
@ -126,12 +137,18 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest
logging.HelperResourceTrace(ctx, "Using TestStep ImportStateCheck")
var states []*terraform.InstanceState
for _, r := range importState.RootModule().Resources {
if r.Primary != nil {
is := r.Primary.DeepCopy()
is.Ephemeral.Type = r.Type // otherwise the check function cannot see the type
states = append(states, is)
for address, r := range importState.RootModule().Resources {
if strings.HasPrefix(address, "data.") {
continue
}
if r.Primary == nil {
continue
}
is := r.Primary.DeepCopy()
is.Ephemeral.Type = r.Type // otherwise the check function cannot see the type
states = append(states, is)
}
logging.HelperResourceDebug(ctx, "Calling TestStep ImportStateCheck")
@ -147,20 +164,27 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest
if step.ImportStateVerify {
logging.HelperResourceTrace(ctx, "Using TestStep ImportStateVerify")
newResources := importState.RootModule().Resources
oldResources := state.RootModule().Resources
// Ensure that we do not match against data sources as they
// cannot be imported and are not what we want to verify.
// Mode is not present in ResourceState so we use the
// stringified ResourceStateKey for comparison.
newResources := make(map[string]*terraform.ResourceState)
for k, v := range importState.RootModule().Resources {
if !strings.HasPrefix(k, "data.") {
newResources[k] = v
}
}
oldResources := make(map[string]*terraform.ResourceState)
for k, v := range state.RootModule().Resources {
if !strings.HasPrefix(k, "data.") {
oldResources[k] = v
}
}
for _, r := range newResources {
// Find the existing resource
var oldR *terraform.ResourceState
for r2Key, r2 := range oldResources {
// Ensure that we do not match against data sources as they
// cannot be imported and are not what we want to verify.
// Mode is not present in ResourceState so we use the
// stringified ResourceStateKey for comparison.
if strings.HasPrefix(r2Key, "data.") {
continue
}
for _, r2 := range oldResources {
if r2.Primary != nil && r2.Primary.ID == r.Primary.ID && r2.Type == r.Type && r2.Provider == r.Provider {
oldR = r2

View File

@ -0,0 +1,97 @@
package resource
import (
"context"
"fmt"
"github.com/davecgh/go-spew/spew"
tfjson "github.com/hashicorp/terraform-json"
"github.com/mitchellh/go-testing-interface"
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/logging"
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/plugintest"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)
func testStepNewRefreshState(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, step TestStep, providers *providerFactories) error {
t.Helper()
spewConf := spew.NewDefaultConfig()
spewConf.SortKeys = true
var err error
// Explicitly ensure prior state exists before refresh.
err = runProviderCommand(ctx, t, func() error {
_, err = getState(ctx, t, wd)
if err != nil {
return err
}
return nil
}, wd, providers)
if err != nil {
t.Fatalf("Error getting state: %s", err)
}
err = runProviderCommand(ctx, t, func() error {
return wd.Refresh(ctx)
}, wd, providers)
if err != nil {
return err
}
var refreshState *terraform.State
err = runProviderCommand(ctx, t, func() error {
refreshState, err = getState(ctx, t, wd)
if err != nil {
return err
}
return nil
}, wd, providers)
if err != nil {
t.Fatalf("Error getting state: %s", err)
}
// Go through the refreshed state and verify
if step.Check != nil {
logging.HelperResourceDebug(ctx, "Calling TestStep Check for RefreshState")
if err := step.Check(refreshState); err != nil {
t.Fatal(err)
}
logging.HelperResourceDebug(ctx, "Called TestStep Check for RefreshState")
}
// do a plan
err = runProviderCommand(ctx, t, func() error {
return wd.CreatePlan(ctx)
}, wd, providers)
if err != nil {
return fmt.Errorf("Error running post-apply plan: %w", err)
}
var plan *tfjson.Plan
err = runProviderCommand(ctx, t, func() error {
var err error
plan, err = wd.SavedPlan(ctx)
return err
}, wd, providers)
if err != nil {
return fmt.Errorf("Error retrieving post-apply plan: %w", err)
}
if !planIsEmpty(plan) && !step.ExpectNonEmptyPlan {
var stdout string
err = runProviderCommand(ctx, t, func() error {
var err error
stdout, err = wd.SavedPlanRawStdout(ctx)
return err
}, wd, providers)
if err != nil {
return fmt.Errorf("Error retrieving formatted plan output: %w", err)
}
return fmt.Errorf("After refreshing state during this test step, a followup plan was not empty.\nstdout:\n\n%s", stdout)
}
return nil
}

View File

@ -25,14 +25,14 @@ const (
// type, a period (.), and the name label. The name for the below example
// configuration would be "myprovider_thing.example".
//
// resource "myprovider_thing" "example" { ... }
// resource "myprovider_thing" "example" { ... }
//
// For data sources, the name parameter is a combination of the keyword "data",
// a period (.), the data source type, a period (.), and the name label. The
// name for the below example configuration would be
// "data.myprovider_thing.example".
//
// data "myprovider_thing" "example" { ... }
// data "myprovider_thing" "example" { ... }
//
// The key parameter is an attribute path in Terraform CLI 0.11 and earlier
// "flatmap" syntax. Keys start with the attribute name of a top-level
@ -46,10 +46,10 @@ const (
// You may check for unset nested attributes, however this will also match keys
// set to an empty string. Use a map with at least 1 non-empty value.
//
// map[string]string{
// "key1": "value",
// "key2": "",
// }
// map[string]string{
// "key1": "value",
// "key2": "",
// }
//
// If the values map is not granular enough, it is possible to match an element
// you were not intending to in the set. Provide the most complete mapping of
@ -97,14 +97,14 @@ func TestCheckTypeSetElemNestedAttrs(name, attr string, values map[string]string
// type, a period (.), and the name label. The name for the below example
// configuration would be "myprovider_thing.example".
//
// resource "myprovider_thing" "example" { ... }
// resource "myprovider_thing" "example" { ... }
//
// For data sources, the name parameter is a combination of the keyword "data",
// a period (.), the data source type, a period (.), and the name label. The
// name for the below example configuration would be
// "data.myprovider_thing.example".
//
// data "myprovider_thing" "example" { ... }
// data "myprovider_thing" "example" { ... }
//
// The key parameter is an attribute path in Terraform CLI 0.11 and earlier
// "flatmap" syntax. Keys start with the attribute name of a top-level
@ -118,10 +118,10 @@ func TestCheckTypeSetElemNestedAttrs(name, attr string, values map[string]string
// You may check for unset nested attributes, however this will also match keys
// set to an empty string. Use a map with at least 1 non-empty value.
//
// map[string]*regexp.Regexp{
// "key1": regexp.MustCompile(`^value`),
// "key2": regexp.MustCompile(`^$`),
// }
// map[string]*regexp.Regexp{
// "key1": regexp.MustCompile(`^value`),
// "key2": regexp.MustCompile(`^$`),
// }
//
// If the values map is not granular enough, it is possible to match an element
// you were not intending to in the set. Provide the most complete mapping of
@ -176,14 +176,14 @@ func TestMatchTypeSetElemNestedAttrs(name, attr string, values map[string]*regex
// type, a period (.), and the name label. The name for the below example
// configuration would be "myprovider_thing.example".
//
// resource "myprovider_thing" "example" { ... }
// resource "myprovider_thing" "example" { ... }
//
// For data sources, the name parameter is a combination of the keyword "data",
// a period (.), the data source type, a period (.), and the name label. The
// name for the below example configuration would be
// "data.myprovider_thing.example".
//
// data "myprovider_thing" "example" { ... }
// data "myprovider_thing" "example" { ... }
//
// The key parameter is an attribute path in Terraform CLI 0.11 and earlier
// "flatmap" syntax. Keys start with the attribute name of a top-level
@ -194,9 +194,9 @@ func TestMatchTypeSetElemNestedAttrs(name, attr string, values map[string]*regex
// The value parameter is the stringified data to check at the given key. Use
// the following attribute type rules to set the value:
//
// - Boolean: "false" or "true".
// - Float/Integer: Stringified number, such as "1.2" or "123".
// - String: No conversion necessary.
// - Boolean: "false" or "true".
// - Float/Integer: Stringified number, such as "1.2" or "123".
// - String: No conversion necessary.
func TestCheckTypeSetElemAttr(name, attr, value string) TestCheckFunc {
return func(s *terraform.State) error {
is, err := primaryInstanceState(s, name)
@ -222,14 +222,14 @@ func TestCheckTypeSetElemAttr(name, attr, value string) TestCheckFunc {
// type, a period (.), and the name label. The name for the below example
// configuration would be "myprovider_thing.example".
//
// resource "myprovider_thing" "example" { ... }
// resource "myprovider_thing" "example" { ... }
//
// For data sources, the name parameter is a combination of the keyword "data",
// a period (.), the data source type, a period (.), and the name label. The
// name for the below example configuration would be
// "data.myprovider_thing.example".
//
// data "myprovider_thing" "example" { ... }
// data "myprovider_thing" "example" { ... }
//
// The first and second names may use any combination of managed resources
// and/or data sources.

View File

@ -3,17 +3,63 @@ package resource
import (
"context"
"fmt"
"regexp"
"strings"
)
var configProviderBlockRegex = regexp.MustCompile(`provider "?[a-zA-Z0-9_-]+"? {`)
// configHasProviderBlock returns true if the Config has declared a provider
// configuration block, e.g. provider "examplecloud" {...}
func (s TestStep) configHasProviderBlock(_ context.Context) bool {
return configProviderBlockRegex.MatchString(s.Config)
}
// configHasTerraformBlock returns true if the Config has declared a terraform
// configuration block, e.g. terraform {...}
func (s TestStep) configHasTerraformBlock(_ context.Context) bool {
return strings.Contains(s.Config, "terraform {")
}
// mergedConfig prepends any necessary terraform configuration blocks to the
// TestStep Config.
//
// If there are ExternalProviders configurations in either the TestCase or
// TestStep, the terraform configuration block should be included with the
// step configuration to prevent errors with providers outside the
// registry.terraform.io hostname or outside the hashicorp namespace.
func (s TestStep) mergedConfig(ctx context.Context, testCase TestCase) string {
var config strings.Builder
// Prevent issues with existing configurations containing the terraform
// configuration block.
if s.configHasTerraformBlock(ctx) {
config.WriteString(s.Config)
return config.String()
}
if testCase.hasProviders(ctx) {
config.WriteString(testCase.providerConfig(ctx, s.configHasProviderBlock(ctx)))
} else {
config.WriteString(s.providerConfig(ctx, s.configHasProviderBlock(ctx)))
}
config.WriteString(s.Config)
return config.String()
}
// providerConfig takes the list of providers in a TestStep and returns a
// config with only empty provider blocks. This is useful for Import, where no
// config is provided, but the providers must be defined.
func (s TestStep) providerConfig(_ context.Context) string {
func (s TestStep) providerConfig(_ context.Context, skipProviderBlock bool) string {
var providerBlocks, requiredProviderBlocks strings.Builder
for name, externalProvider := range s.ExternalProviders {
providerBlocks.WriteString(fmt.Sprintf("provider %q {}\n", name))
if !skipProviderBlock {
providerBlocks.WriteString(fmt.Sprintf("provider %q {}\n", name))
}
if externalProvider.Source == "" && externalProvider.VersionConstraint == "" {
continue

View File

@ -43,24 +43,50 @@ func (s TestStep) hasProviders(_ context.Context) bool {
// validate ensures the TestStep is valid based on the following criteria:
//
// - Config or ImportState is set.
// - Providers are not specified (ExternalProviders,
// ProtoV5ProviderFactories, ProtoV6ProviderFactories, ProviderFactories)
// if specified at the TestCase level.
// - Providers are specified (ExternalProviders, ProtoV5ProviderFactories,
// ProtoV6ProviderFactories, ProviderFactories) if not specified at the
// TestCase level.
// - No overlapping ExternalProviders and ProviderFactories entries
// - ResourceName is not empty when ImportState is true, ImportStateIdFunc
// is not set, and ImportStateId is not set.
//
// - Config or ImportState or RefreshState is set.
// - Config and RefreshState are not both set.
// - RefreshState and Destroy are not both set.
// - RefreshState is not the first TestStep.
// - Providers are not specified (ExternalProviders,
// ProtoV5ProviderFactories, ProtoV6ProviderFactories, ProviderFactories)
// if specified at the TestCase level.
// - Providers are specified (ExternalProviders, ProtoV5ProviderFactories,
// ProtoV6ProviderFactories, ProviderFactories) if not specified at the
// TestCase level.
// - No overlapping ExternalProviders and ProviderFactories entries
// - ResourceName is not empty when ImportState is true, ImportStateIdFunc
// is not set, and ImportStateId is not set.
func (s TestStep) validate(ctx context.Context, req testStepValidateRequest) error {
ctx = logging.TestStepNumberContext(ctx, req.StepNumber)
logging.HelperResourceTrace(ctx, "Validating TestStep")
if s.Config == "" && !s.ImportState {
err := fmt.Errorf("TestStep missing Config or ImportState")
if s.Config == "" && !s.ImportState && !s.RefreshState {
err := fmt.Errorf("TestStep missing Config or ImportState or RefreshState")
logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err})
return err
}
if s.Config != "" && s.RefreshState {
err := fmt.Errorf("TestStep cannot have Config and RefreshState")
logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err})
return err
}
if s.RefreshState && s.Destroy {
err := fmt.Errorf("TestStep cannot have RefreshState and Destroy")
logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err})
return err
}
if s.RefreshState && req.StepNumber == 1 {
err := fmt.Errorf("TestStep cannot have RefreshState as first step")
logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err})
return err
}
if s.ImportState && s.RefreshState {
err := fmt.Errorf("TestStep cannot have ImportState and RefreshState in same step")
logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err})
return err
}

View File

@ -33,16 +33,16 @@ var ReservedResourceFields = []string{
// Resource is an abstraction for multiple Terraform concepts:
//
// - Managed Resource: An infrastructure component with a schema, lifecycle
// operations such as create, read, update, and delete
// (CRUD), and optional implementation details such as
// import support, upgrade state support, and difference
// customization.
// - Data Resource: Also known as a data source. An infrastructure component
// with a schema and only the read lifecycle operation.
// - Block: When implemented within a Schema type Elem field, a configuration
// block that contains nested schema information such as attributes
// and blocks.
// - Managed Resource: An infrastructure component with a schema, lifecycle
// operations such as create, read, update, and delete
// (CRUD), and optional implementation details such as
// import support, upgrade state support, and difference
// customization.
// - Data Resource: Also known as a data source. An infrastructure component
// with a schema and only the read lifecycle operation.
// - Block: When implemented within a Schema type Elem field, a configuration
// block that contains nested schema information such as attributes
// and blocks.
//
// To fully implement managed resources, the Provider type ResourcesMap field
// should include a reference to an implementation of this type. To fully
@ -666,13 +666,13 @@ type StateUpgrader struct {
// or block names mapped to values that can be type asserted similar to
// fetching values using the ResourceData Get* methods:
//
// - TypeBool: bool
// - TypeFloat: float
// - TypeInt: int
// - TypeList: []interface{}
// - TypeMap: map[string]interface{}
// - TypeSet: *Set
// - TypeString: string
// - TypeBool: bool
// - TypeFloat: float
// - TypeInt: int
// - TypeList: []interface{}
// - TypeMap: map[string]interface{}
// - TypeSet: *Set
// - TypeString: string
//
// In certain scenarios, the map may be nil, so checking for that condition
// upfront is recommended to prevent potential panics.

View File

@ -160,7 +160,6 @@ func unsupportedTimeoutKeyError(key string) error {
//
// StateEncode encodes the timeout into the ResourceData's InstanceState for
// saving to state
//
func (t *ResourceTimeout) DiffEncode(id *terraform.InstanceDiff) error {
return t.metaEncode(id)
}

View File

@ -311,32 +311,33 @@ type Schema struct {
// "parent_block_name.0.child_attribute_name".
RequiredWith []string
// Deprecated defines warning diagnostic details to display to
// practitioners configuring this attribute or block. The warning
// Deprecated defines warning diagnostic details to display when
// practitioner configurations use this attribute or block. The warning
// diagnostic summary is automatically set to "Argument is deprecated"
// along with configuration source file and line information.
//
// This warning diagnostic is only displayed during Terraform's validation
// phase when this field is a non-empty string, when the attribute is
// Required or Optional, and if the practitioner configuration attempts to
// set the attribute value to a known value. It cannot detect practitioner
// configuration values that are unknown ("known after apply").
//
// This field has no effect when the attribute is Computed-only (read-only;
// not Required or Optional) and a practitioner attempts to reference
// this attribute value in their configuration. There is a Terraform
// feature request to support this type of functionality:
//
// https://github.com/hashicorp/terraform/issues/7569
//
// Set this field to a practitioner actionable message such as:
//
// - "Configure other_attribute instead. This attribute will be removed
// in the next major version of the provider."
// - "Remove this attribute's configuration as it no longer is used and
// the attribute will be removed in the next major version of the
// provider."
// - "Configure other_attribute instead. This attribute will be removed
// in the next major version of the provider."
// - "Remove this attribute's configuration as it no longer is used and
// the attribute will be removed in the next major version of the
// provider."
//
// In Terraform 1.2.7 and later, this warning diagnostic is displayed any
// time a practitioner attempts to configure a known value for this
// attribute and certain scenarios where this attribute is referenced.
//
// In Terraform 1.2.6 and earlier, this warning diagnostic is only
// displayed when the attribute is Required or Optional, and if the
// practitioner configuration attempts to set the attribute value to a
// known value. It cannot detect practitioner configuration values that
// are unknown ("known after apply").
//
// Additional information about deprecation enhancements for read-only
// attributes can be found in:
//
// - https://github.com/hashicorp/terraform/issues/7569
Deprecated string
// ValidateFunc allows individual fields to define arbitrary validation
@ -721,6 +722,9 @@ func (m schemaMap) Diff(
// Preserve the DestroyTainted flag
result2.DestroyTainted = result.DestroyTainted
result2.RawConfig = result.RawConfig
result2.RawPlan = result.RawPlan
result2.RawState = result.RawState
// Reset the data to not contain state. We have to call init()
// again in order to reset the FieldReaders.
@ -1089,9 +1093,10 @@ func checkKeysAgainstSchemaFlags(k string, keys []string, topSchemaMap schemaMap
return nil
}
var validFieldNameRe = regexp.MustCompile("^[a-z0-9_]+$")
func isValidFieldName(name string) bool {
re := regexp.MustCompile("^[a-z0-9_]+$")
return re.MatchString(name)
return validFieldNameRe.MatchString(name)
}
// resourceDiffer is an interface that is used by the private diff functions.
@ -1731,15 +1736,7 @@ func (m schemaMap) validate(
// The SDK has to allow the unknown value through initially, so that
// Required fields set via an interpolated value are accepted.
if !isWhollyKnown(raw) {
if schema.Deprecated != "" {
return append(diags, diag.Diagnostic{
Severity: diag.Warning,
Summary: "Argument is deprecated",
Detail: schema.Deprecated,
AttributePath: path,
})
}
return diags
return nil
}
err = validateConflictingAttributes(k, schema, c)
@ -1942,7 +1939,7 @@ func (m schemaMap) validateList(
return append(diags, diag.Diagnostic{
Severity: diag.Error,
Summary: "Too many list items",
Detail: fmt.Sprintf("Attribute supports %d item maximum, but config has %d declared.", schema.MaxItems, rawV.Len()),
Detail: fmt.Sprintf("Attribute %s supports %d item maximum, but config has %d declared.", k, schema.MaxItems, rawV.Len()),
AttributePath: path,
})
}
@ -1951,7 +1948,7 @@ func (m schemaMap) validateList(
return append(diags, diag.Diagnostic{
Severity: diag.Error,
Summary: "Not enough list items",
Detail: fmt.Sprintf("Attribute requires %d item minimum, but config has only %d declared.", schema.MinItems, rawV.Len()),
Detail: fmt.Sprintf("Attribute %s requires %d item minimum, but config has only %d declared.", k, schema.MinItems, rawV.Len()),
AttributePath: path,
})
}
@ -2130,7 +2127,7 @@ func validateMapValues(k string, m map[string]interface{}, schema *Schema, path
})
}
default:
panic(fmt.Sprintf("Unknown validation type: %#v", schema.Type))
panic(fmt.Sprintf("Unknown validation type: %#v", valueType))
}
}
return diags

View File

@ -31,6 +31,9 @@ const (
// The TestStep number of the test being executed. Starts at 1.
KeyTestStepNumber = "test_step_number"
// Terraform configuration used during acceptance testing Terraform operations.
KeyTestTerraformConfiguration = "test_terraform_configuration"
// The Terraform CLI logging level (TF_LOG) used for an acceptance test.
KeyTestTerraformLogLevel = "test_terraform_log_level"
@ -49,6 +52,9 @@ const (
// The path to the Terraform CLI used for an acceptance test.
KeyTestTerraformPath = "test_terraform_path"
// Terraform plan output generated during a TestStep.
KeyTestTerraformPlan = "test_terraform_plan"
// The working directory of the acceptance test.
KeyTestWorkingDirectory = "test_working_directory"
)

View File

@ -3,7 +3,6 @@ package plugintest
import (
"context"
"fmt"
"io/ioutil"
"os"
"strings"
@ -35,7 +34,7 @@ func DiscoverConfig(ctx context.Context, sourceDir string) (*Config, error) {
tfPath := os.Getenv(EnvTfAccTerraformPath)
tempDir := os.Getenv(EnvTfAccTempDir)
tfDir, err := ioutil.TempDir(tempDir, "plugintest-terraform")
tfDir, err := os.MkdirTemp(tempDir, "plugintest-terraform")
if err != nil {
return nil, fmt.Errorf("failed to create temp dir: %w", err)
}

View File

@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"io/ioutil"
"os"
"strings"
@ -70,7 +69,7 @@ func AutoInitHelper(ctx context.Context, sourceDir string) (*Helper, error) {
// automatically clean those up.
func InitHelper(ctx context.Context, config *Config) (*Helper, error) {
tempDir := os.Getenv(EnvTfAccTempDir)
baseDir, err := ioutil.TempDir(tempDir, "plugintest")
baseDir, err := os.MkdirTemp(tempDir, "plugintest")
if err != nil {
return nil, fmt.Errorf("failed to create temporary directory for test helper: %s", err)
}
@ -105,7 +104,7 @@ func (h *Helper) Close() error {
// program exits, the Close method on the helper itself will attempt to
// delete it.
func (h *Helper) NewWorkingDir(ctx context.Context, t TestControl) (*WorkingDir, error) {
dir, err := ioutil.TempDir(h.baseDir, "work")
dir, err := os.MkdirTemp(h.baseDir, "work")
if err != nil {
return nil, err
}

View File

@ -28,79 +28,40 @@ func symlinkFile(src string, dest string) error {
return nil
}
// symlinkDir is a simplistic function for recursively symlinking all files in a directory to a new path.
// It is intended only for limited internal use and does not cover all edge cases.
func symlinkDir(srcDir string, destDir string) (err error) {
srcInfo, err := os.Stat(srcDir)
if err != nil {
return err
}
err = os.MkdirAll(destDir, srcInfo.Mode())
if err != nil {
return err
}
directory, _ := os.Open(srcDir)
defer directory.Close()
objects, err := directory.Readdir(-1)
for _, obj := range objects {
srcPath := filepath.Join(srcDir, obj.Name())
destPath := filepath.Join(destDir, obj.Name())
if obj.IsDir() {
err = symlinkDir(srcPath, destPath)
if err != nil {
return err
}
} else {
err = symlinkFile(srcPath, destPath)
if err != nil {
return err
}
}
}
return
}
// symlinkDirectoriesOnly finds only the first-level child directories in srcDir
// and symlinks them into destDir.
// Unlike symlinkDir, this is done non-recursively in order to limit the number
// of file descriptors used.
func symlinkDirectoriesOnly(srcDir string, destDir string) (err error) {
func symlinkDirectoriesOnly(srcDir string, destDir string) error {
srcInfo, err := os.Stat(srcDir)
if err != nil {
return err
return fmt.Errorf("unable to stat source directory %q: %w", srcDir, err)
}
err = os.MkdirAll(destDir, srcInfo.Mode())
if err != nil {
return err
return fmt.Errorf("unable to make destination directory %q: %w", destDir, err)
}
directory, err := os.Open(srcDir)
dirEntries, err := os.ReadDir(srcDir)
if err != nil {
return err
}
defer directory.Close()
objects, err := directory.Readdir(-1)
if err != nil {
return err
return fmt.Errorf("unable to read source directory %q: %w", srcDir, err)
}
for _, obj := range objects {
srcPath := filepath.Join(srcDir, obj.Name())
destPath := filepath.Join(destDir, obj.Name())
if obj.IsDir() {
err = symlinkFile(srcPath, destPath)
if err != nil {
return err
}
for _, dirEntry := range dirEntries {
if !dirEntry.IsDir() {
continue
}
srcPath := filepath.Join(srcDir, dirEntry.Name())
destPath := filepath.Join(destDir, dirEntry.Name())
err := symlinkFile(srcPath, destPath)
if err != nil {
return fmt.Errorf("unable to symlink directory %q to %q: %w", srcPath, destPath, err)
}
}
return
return nil
}

View File

@ -1,16 +1,15 @@
package plugintest
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/hashicorp/terraform-exec/tfexec"
tfjson "github.com/hashicorp/terraform-json"
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/logging"
)
@ -75,6 +74,8 @@ func (wd *WorkingDir) GetHelper() *Helper {
// Destroy to establish the configuration. Any previously-set configuration is
// discarded and any saved plan is cleared.
func (wd *WorkingDir) SetConfig(ctx context.Context, cfg string) error {
logging.HelperResourceTrace(ctx, "Setting Terraform configuration", map[string]any{logging.KeyTestTerraformConfiguration: cfg})
outFilename := filepath.Join(wd.baseDir, ConfigFileName)
rmFilename := filepath.Join(wd.baseDir, ConfigFileNameJSON)
bCfg := []byte(cfg)
@ -84,7 +85,7 @@ func (wd *WorkingDir) SetConfig(ctx context.Context, cfg string) error {
if err := os.Remove(rmFilename); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("unable to remove %q: %w", rmFilename, err)
}
err := ioutil.WriteFile(outFilename, bCfg, 0700)
err := os.WriteFile(outFilename, bCfg, 0700)
if err != nil {
return err
}
@ -173,11 +174,29 @@ func (wd *WorkingDir) planFilename() string {
func (wd *WorkingDir) CreatePlan(ctx context.Context) error {
logging.HelperResourceTrace(ctx, "Calling Terraform CLI plan command")
_, err := wd.tf.Plan(context.Background(), tfexec.Reattach(wd.reattachInfo), tfexec.Refresh(false), tfexec.Out(PlanFileName))
hasChanges, err := wd.tf.Plan(context.Background(), tfexec.Reattach(wd.reattachInfo), tfexec.Refresh(false), tfexec.Out(PlanFileName))
logging.HelperResourceTrace(ctx, "Called Terraform CLI plan command")
return err
if err != nil {
return err
}
if !hasChanges {
logging.HelperResourceTrace(ctx, "Created plan with no changes")
return nil
}
stdout, err := wd.SavedPlanRawStdout(ctx)
if err != nil {
return fmt.Errorf("error retrieving formatted plan output: %w", err)
}
logging.HelperResourceTrace(ctx, "Created plan with changes", map[string]any{logging.KeyTestTerraformPlan: stdout})
return nil
}
// CreateDestroyPlan runs "terraform plan -destroy" to create a saved plan
@ -185,11 +204,29 @@ func (wd *WorkingDir) CreatePlan(ctx context.Context) error {
func (wd *WorkingDir) CreateDestroyPlan(ctx context.Context) error {
logging.HelperResourceTrace(ctx, "Calling Terraform CLI plan -destroy command")
_, err := wd.tf.Plan(context.Background(), tfexec.Reattach(wd.reattachInfo), tfexec.Refresh(false), tfexec.Out(PlanFileName), tfexec.Destroy(true))
hasChanges, err := wd.tf.Plan(context.Background(), tfexec.Reattach(wd.reattachInfo), tfexec.Refresh(false), tfexec.Out(PlanFileName), tfexec.Destroy(true))
logging.HelperResourceTrace(ctx, "Called Terraform CLI plan -destroy command")
return err
if err != nil {
return err
}
if !hasChanges {
logging.HelperResourceTrace(ctx, "Created destroy plan with no changes")
return nil
}
stdout, err := wd.SavedPlanRawStdout(ctx)
if err != nil {
return fmt.Errorf("error retrieving formatted plan output: %w", err)
}
logging.HelperResourceTrace(ctx, "Created destroy plan with changes", map[string]any{logging.KeyTestTerraformPlan: stdout})
return nil
}
// Apply runs "terraform apply". If CreatePlan has previously completed
@ -242,11 +279,11 @@ func (wd *WorkingDir) SavedPlan(ctx context.Context) (*tfjson.Plan, error) {
return nil, fmt.Errorf("there is no current saved plan")
}
logging.HelperResourceTrace(ctx, "Calling Terraform CLI apply command")
logging.HelperResourceTrace(ctx, "Calling Terraform CLI show command for JSON plan")
plan, err := wd.tf.ShowPlanFile(context.Background(), wd.planFilename(), tfexec.Reattach(wd.reattachInfo))
logging.HelperResourceTrace(ctx, "Calling Terraform CLI apply command")
logging.HelperResourceTrace(ctx, "Calling Terraform CLI show command for JSON plan")
return plan, err
}
@ -260,22 +297,17 @@ func (wd *WorkingDir) SavedPlanRawStdout(ctx context.Context) (string, error) {
return "", fmt.Errorf("there is no current saved plan")
}
var ret bytes.Buffer
logging.HelperResourceTrace(ctx, "Calling Terraform CLI show command for stdout plan")
wd.tf.SetStdout(&ret)
defer wd.tf.SetStdout(ioutil.Discard)
stdout, err := wd.tf.ShowPlanFileRaw(context.Background(), wd.planFilename(), tfexec.Reattach(wd.reattachInfo))
logging.HelperResourceTrace(ctx, "Calling Terraform CLI show command")
_, err := wd.tf.ShowPlanFileRaw(context.Background(), wd.planFilename(), tfexec.Reattach(wd.reattachInfo))
logging.HelperResourceTrace(ctx, "Called Terraform CLI show command")
logging.HelperResourceTrace(ctx, "Called Terraform CLI show command for stdout plan")
if err != nil {
return "", err
}
return ret.String(), nil
return stdout, nil
}
// State returns an object describing the current state.
@ -283,11 +315,11 @@ func (wd *WorkingDir) SavedPlanRawStdout(ctx context.Context) (string, error) {
// If the state cannot be read, State returns an error.
func (wd *WorkingDir) State(ctx context.Context) (*tfjson.State, error) {
logging.HelperResourceTrace(ctx, "Calling Terraform CLI show command")
logging.HelperResourceTrace(ctx, "Calling Terraform CLI show command for JSON state")
state, err := wd.tf.Show(context.Background(), tfexec.Reattach(wd.reattachInfo))
logging.HelperResourceTrace(ctx, "Called Terraform CLI show command")
logging.HelperResourceTrace(ctx, "Called Terraform CLI show command for JSON state")
return state, err
}
@ -303,6 +335,17 @@ func (wd *WorkingDir) Import(ctx context.Context, resource, id string) error {
return err
}
// Taint runs terraform taint
func (wd *WorkingDir) Taint(ctx context.Context, address string) error {
logging.HelperResourceTrace(ctx, "Calling Terraform CLI taint command")
err := wd.tf.Taint(context.Background(), address)
logging.HelperResourceTrace(ctx, "Called Terraform CLI taint command")
return err
}
// Refresh runs terraform refresh
func (wd *WorkingDir) Refresh(ctx context.Context) error {
logging.HelperResourceTrace(ctx, "Calling Terraform CLI refresh command")

View File

@ -52,7 +52,7 @@ func (diags Diagnostics) ForRPC() Diagnostics {
// that aren't accompanied by at least one error) since such APIs have no
// mechanism through which to report these.
//
// return result, diags.Error()
// return result, diags.Error()
func (diags Diagnostics) Err() error {
if !diags.HasErrors() {
return nil

View File

@ -55,6 +55,10 @@ type ServeOpts struct {
// information needed for Terraform to connect to the provider to stdout.
// os.Interrupt will be captured and used to stop the server.
//
// Ensure the ProviderAddr field is correctly set when this is enabled,
// otherwise the TF_REATTACH_PROVIDERS environment variable will not
// correctly point Terraform to the running provider binary.
//
// This option cannot be combined with TestConfig.
Debug bool
@ -76,8 +80,11 @@ type ServeOpts struct {
// the terraform-plugin-log logging sink.
UseTFLogSink testing.T
// ProviderAddr is the address of the provider under test, like
// registry.terraform.io/hashicorp/random.
// ProviderAddr is the address of the provider under test or debugging,
// such as registry.terraform.io/hashicorp/random. This value is used in
// the TF_REATTACH_PROVIDERS environment variable during debugging so
// Terraform can correctly match the provider address in the Terraform
// configuration to the running provider binary.
ProviderAddr string
}

View File

@ -14,8 +14,8 @@ import (
"sync"
"github.com/hashicorp/go-cty/cty"
multierror "github.com/hashicorp/go-multierror"
uuid "github.com/hashicorp/go-uuid"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-uuid"
"github.com/mitchellh/copystructure"
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/addrs"
@ -1145,7 +1145,6 @@ func parseResourceStateKey(k string) (*ResourceStateKey, error) {
//
// Extra is just extra data that a provider can return that we store
// for later, but is not exposed in any way to the user.
//
type ResourceState struct {
// This is filled in and managed by Terraform, and is the resource
// type itself such as "mycloud_instance". If a resource provider sets
@ -1226,26 +1225,6 @@ func (s *ResourceState) Equal(other *ResourceState) bool {
return s.Primary.Equal(other.Primary)
}
// Taint marks a resource as tainted.
func (s *ResourceState) Taint() {
s.Lock()
defer s.Unlock()
if s.Primary != nil {
s.Primary.Tainted = true
}
}
// Untaint unmarks a resource as tainted.
func (s *ResourceState) Untaint() {
s.Lock()
defer s.Unlock()
if s.Primary != nil {
s.Primary.Tainted = false
}
}
func (s *ResourceState) init() {
s.Lock()
defer s.Unlock()