From 7e1e4541e58fc2df8cdeac6069cb26ca10e727f0 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 6 Jan 2026 14:32:05 -0500 Subject: [PATCH 01/19] initial implementation of declarative state store SDK + provider --- go.mod | 8 +- go.sum | 16 +- internal/testing/testprovider/provider.go | 12 + internal/testing/testprovider/statestore.go | 93 +++++ internal/testing/testsdk/provider/provider.go | 2 + .../testsdk/providerserver/providerserver.go | 368 +++++++++++++++++- .../testsdk/providerserver/statestores.go | 24 ++ .../testing/testsdk/statestore/statestore.go | 102 +++++ 8 files changed, 610 insertions(+), 15 deletions(-) create mode 100644 internal/testing/testprovider/statestore.go create mode 100644 internal/testing/testsdk/providerserver/statestores.go create mode 100644 internal/testing/testsdk/statestore/statestore.go diff --git a/go.mod b/go.mod index fe3f8a8d..89cff9cc 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/hashicorp/logutils v1.0.0 github.com/hashicorp/terraform-exec v0.24.0 github.com/hashicorp/terraform-json v0.27.2 - github.com/hashicorp/terraform-plugin-go v0.29.0 + github.com/hashicorp/terraform-plugin-go v0.29.1-0.20251107103451-b1dbec9688da github.com/hashicorp/terraform-plugin-log v0.10.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1 github.com/mitchellh/go-testing-interface v1.14.1 @@ -54,7 +54,7 @@ require ( golang.org/x/text v0.33.0 // indirect golang.org/x/tools v0.40.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect - google.golang.org/grpc v1.75.1 // indirect - google.golang.org/protobuf v1.36.9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect + google.golang.org/grpc v1.76.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect ) diff --git a/go.sum b/go.sum index efd47200..adedd846 100644 --- a/go.sum +++ b/go.sum @@ -81,8 +81,8 @@ github.com/hashicorp/terraform-exec v0.24.0 h1:mL0xlk9H5g2bn0pPF6JQZk5YlByqSqrO5 github.com/hashicorp/terraform-exec v0.24.0/go.mod h1:lluc/rDYfAhYdslLJQg3J0oDqo88oGQAdHR+wDqFvo4= github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoKST/tRDBJKU= github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= -github.com/hashicorp/terraform-plugin-go v0.29.0 h1:1nXKl/nSpaYIUBU1IG/EsDOX0vv+9JxAltQyDMpq5mU= -github.com/hashicorp/terraform-plugin-go v0.29.0/go.mod h1:vYZbIyvxyy0FWSmDHChCqKvI40cFTDGSb3D8D70i9GM= +github.com/hashicorp/terraform-plugin-go v0.29.1-0.20251107103451-b1dbec9688da h1:RvFdseryCMkoBWxucUSEnQkeROpBiungf3bNQV1efgc= +github.com/hashicorp/terraform-plugin-go v0.29.1-0.20251107103451-b1dbec9688da/go.mod h1:KHRnT9vExG+r1fLxwOzOP6C6YXiaKHtsCIrky7teDYc= github.com/hashicorp/terraform-plugin-log v0.10.0 h1:eu2kW6/QBVdN4P3Ju2WiB2W3ObjkAsyfBsL3Wh1fj3g= github.com/hashicorp/terraform-plugin-log v0.10.0/go.mod h1:/9RR5Cv2aAbrqcTSdNmY1NRHP4E3ekrXRGjqORpXyB0= github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1 h1:mlAq/OrMlg04IuJT7NpefI1wwtdpWudnEmjuQs04t/4= @@ -221,14 +221,14 @@ gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= -google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= -google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/internal/testing/testprovider/provider.go b/internal/testing/testprovider/provider.go index b72c3532..f02f0702 100644 --- a/internal/testing/testprovider/provider.go +++ b/internal/testing/testprovider/provider.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/list" "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/provider" "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/statestore" ) var _ provider.Provider = Provider{} @@ -22,6 +23,7 @@ type Provider struct { DataSources map[string]DataSource ListResources map[string]ListResource Resources map[string]Resource + StateStores map[string]*StateStore SchemaResponse *provider.SchemaResponse StopResponse *provider.StopResponse ValidateConfigResponse *provider.ValidateConfigResponse @@ -63,6 +65,16 @@ func (p Provider) ResourcesMap() map[string]resource.Resource { return resources } +func (p Provider) StateStoresMap() map[string]statestore.StateStore { + stateStores := make(map[string]statestore.StateStore, len(p.StateStores)) + + for typeName, s := range p.StateStores { + stateStores[typeName] = s + } + + return stateStores +} + func (p Provider) Stop(ctx context.Context, req provider.StopRequest, resp *provider.StopResponse) { if p.StopResponse != nil { resp.Error = p.StopResponse.Error diff --git a/internal/testing/testprovider/statestore.go b/internal/testing/testprovider/statestore.go new file mode 100644 index 00000000..fb895c5a --- /dev/null +++ b/internal/testing/testprovider/statestore.go @@ -0,0 +1,93 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/statestore" +) + +var _ statestore.StateStore = &StateStore{} + +type StateStore struct { + configuredChunkSize int64 + + SchemaResponse *statestore.SchemaResponse + ConfigureResponse *statestore.ConfigureResponse + ValidateConfigResponse *statestore.ValidateConfigResponse + GetStatesResponse *statestore.GetStatesResponse + DeleteStateResponse *statestore.DeleteStateResponse + LockStateResponse *statestore.LockStateResponse + UnlockStateResponse *statestore.UnlockStateResponse + ReadStateBytesResponse *statestore.ReadStateBytesResponse + WriteStateBytesResponse *statestore.WriteStateBytesResponse +} + +func (s *StateStore) Schema(ctx context.Context, req statestore.SchemaRequest, resp *statestore.SchemaResponse) { + if s.SchemaResponse != nil { + resp.Diagnostics = s.SchemaResponse.Diagnostics + resp.Schema = s.SchemaResponse.Schema + } +} + +func (s *StateStore) Configure(ctx context.Context, req statestore.ConfigureRequest, resp *statestore.ConfigureResponse) { + if s.ConfigureResponse != nil { + resp.Diagnostics = s.ConfigureResponse.Diagnostics + resp.ServerCapabilities = s.ConfigureResponse.ServerCapabilities + } + + // Store configured chunk size + s.configuredChunkSize = resp.ServerCapabilities.ChunkSize +} + +func (s *StateStore) ConfiguredChunkSize() int64 { + return s.configuredChunkSize +} + +func (s *StateStore) ValidateConfig(ctx context.Context, req statestore.ValidateConfigRequest, resp *statestore.ValidateConfigResponse) { + if s.ValidateConfigResponse != nil { + resp.Diagnostics = s.ValidateConfigResponse.Diagnostics + } +} + +// TODO:PSS: Probably need to adjust some of these to be callback functions, rather then field responses. +func (s *StateStore) GetStates(ctx context.Context, req statestore.GetStatesRequest, resp *statestore.GetStatesResponse) { + if s.GetStatesResponse != nil { + resp.Diagnostics = s.GetStatesResponse.Diagnostics + resp.StateIDs = s.GetStatesResponse.StateIDs + } +} + +func (s *StateStore) DeleteState(ctx context.Context, req statestore.DeleteStateRequest, resp *statestore.DeleteStateResponse) { + if s.DeleteStateResponse != nil { + resp.Diagnostics = s.DeleteStateResponse.Diagnostics + } +} + +func (s *StateStore) LockState(ctx context.Context, req statestore.LockStateRequest, resp *statestore.LockStateResponse) { + if s.LockStateResponse != nil { + resp.LockID = s.LockStateResponse.LockID + resp.Diagnostics = s.LockStateResponse.Diagnostics + } +} + +func (s *StateStore) UnlockState(ctx context.Context, req statestore.UnlockStateRequest, resp *statestore.UnlockStateResponse) { + if s.UnlockStateResponse != nil { + resp.Diagnostics = s.UnlockStateResponse.Diagnostics + } +} + +func (s *StateStore) ReadStateBytes(ctx context.Context, req statestore.ReadStateBytesRequest, resp *statestore.ReadStateBytesResponse) { + if s.ReadStateBytesResponse != nil { + resp.Diagnostics = s.ReadStateBytesResponse.Diagnostics + resp.StateBytes = s.ReadStateBytesResponse.StateBytes + } +} + +func (s *StateStore) WriteStateBytes(ctx context.Context, req statestore.WriteStateBytesRequest, resp *statestore.WriteStateBytesResponse) { + if s.WriteStateBytesResponse != nil { + resp.Diagnostics = s.WriteStateBytesResponse.Diagnostics + } +} diff --git a/internal/testing/testsdk/provider/provider.go b/internal/testing/testsdk/provider/provider.go index 93206f2b..1d951f36 100644 --- a/internal/testing/testsdk/provider/provider.go +++ b/internal/testing/testsdk/provider/provider.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/datasource" "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/list" "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/statestore" ) type Provider interface { @@ -18,6 +19,7 @@ type Provider interface { DataSourcesMap() map[string]datasource.DataSource ListResourcesMap() map[string]list.ListResource ResourcesMap() map[string]resource.Resource + StateStoresMap() map[string]statestore.StateStore Schema(context.Context, SchemaRequest, *SchemaResponse) Stop(context.Context, StopRequest, *StopResponse) ValidateConfig(context.Context, ValidateConfigRequest, *ValidateConfigResponse) diff --git a/internal/testing/testsdk/providerserver/providerserver.go b/internal/testing/testsdk/providerserver/providerserver.go index ed825ccf..f36c1e56 100644 --- a/internal/testing/testsdk/providerserver/providerserver.go +++ b/internal/testing/testsdk/providerserver/providerserver.go @@ -4,10 +4,13 @@ package providerserver import ( + "bytes" "context" "errors" "fmt" + "io" "iter" + "slices" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-go/tftypes" @@ -16,12 +19,11 @@ import ( "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/list" "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/provider" "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/statestore" ) var _ tfprotov6.ProviderServer = ProviderServer{} -// var _ tfprotov6.ProviderServerWithListResource = ProviderServer{} - // NewProviderServer returns a lightweight protocol version 6 provider server // for consumption with ProtoV6ProviderFactories. func NewProviderServer(p provider.Provider) func() (tfprotov6.ProviderServer, error) { @@ -319,6 +321,7 @@ func (s ProviderServer) GetProviderSchema(ctx context.Context, req *tfprotov6.Ge ListResourceSchemas: map[string]*tfprotov6.Schema{}, Provider: providerResp.Schema, ResourceSchemas: map[string]*tfprotov6.Schema{}, + StateStoreSchemas: map[string]*tfprotov6.Schema{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ PlanDestroy: true, }, @@ -357,6 +360,17 @@ func (s ProviderServer) GetProviderSchema(ctx context.Context, req *tfprotov6.Ge resp.ResourceSchemas[typeName] = schemaResp.Schema } + for typeName, s := range s.Provider.StateStoresMap() { + schemaReq := statestore.SchemaRequest{} + schemaResp := &statestore.SchemaResponse{} + + s.Schema(ctx, schemaReq, schemaResp) + + resp.Diagnostics = append(resp.Diagnostics, schemaResp.Diagnostics...) + + resp.StateStoreSchemas[typeName] = schemaResp.Schema + } + return resp, nil } @@ -912,7 +926,6 @@ func (s ProviderServer) UpgradeResourceState(ctx context.Context, req *tfprotov6 } func (s ProviderServer) UpgradeResourceIdentity(context.Context, *tfprotov6.UpgradeResourceIdentityRequest) (*tfprotov6.UpgradeResourceIdentityResponse, error) { - // TODO: This isn't currently being used by the testing framework provider, so no need to implement it until then. return nil, errors.New("UpgradeResourceIdentity is not currently implemented in testprovider") } @@ -1231,3 +1244,352 @@ func processListResult(req list.ListRequest, result list.ListResult) tfprotov6.L return listResourceResult } + +func (s ProviderServer) ConfigureStateStore(ctx context.Context, req *tfprotov6.ConfigureStateStoreRequest) (*tfprotov6.ConfigureStateStoreResponse, error) { + resp := &tfprotov6.ConfigureStateStoreResponse{} + + store, diag := ProviderStateStore(s.Provider, req.TypeName) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + schemaReq := statestore.SchemaRequest{} + schemaResp := &statestore.SchemaResponse{} + + store.Schema(ctx, schemaReq, schemaResp) + + resp.Diagnostics = schemaResp.Diagnostics + + if len(resp.Diagnostics) > 0 { + return resp, nil + } + + config, diag := DynamicValueToValue(schemaResp.Schema, req.Config) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + configureReq := statestore.ConfigureRequest{ + Config: config, + ClientCapabilities: req.Capabilities, + } + configureResp := &statestore.ConfigureResponse{ + // Round-trip the core-provided chunk size as the default + ServerCapabilities: tfprotov6.StateStoreServerCapabilities{ + ChunkSize: req.Capabilities.ChunkSize, + }, + } + + store.Configure(ctx, configureReq, configureResp) + + resp.Diagnostics = configureResp.Diagnostics + resp.Capabilities = configureResp.ServerCapabilities + + return resp, nil +} + +func (s ProviderServer) ValidateStateStoreConfig(ctx context.Context, req *tfprotov6.ValidateStateStoreRequest) (*tfprotov6.ValidateStateStoreResponse, error) { + resp := &tfprotov6.ValidateStateStoreResponse{} + + store, diag := ProviderStateStore(s.Provider, req.TypeName) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + schemaReq := statestore.SchemaRequest{} + schemaResp := &statestore.SchemaResponse{} + + store.Schema(ctx, schemaReq, schemaResp) + + resp.Diagnostics = schemaResp.Diagnostics + + if len(resp.Diagnostics) > 0 { + return resp, nil + } + + config, diag := DynamicValueToValue(schemaResp.Schema, req.Config) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + validateReq := statestore.ValidateConfigRequest{ + Config: config, + } + validateResp := &statestore.ValidateConfigResponse{} + + store.ValidateConfig(ctx, validateReq, validateResp) + + resp.Diagnostics = validateResp.Diagnostics + + return resp, nil +} + +func (s ProviderServer) GetStates(ctx context.Context, req *tfprotov6.GetStatesRequest) (*tfprotov6.GetStatesResponse, error) { + resp := &tfprotov6.GetStatesResponse{} + + store, diag := ProviderStateStore(s.Provider, req.TypeName) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + getStatesReq := statestore.GetStatesRequest{} + getStatesResp := &statestore.GetStatesResponse{} + + store.GetStates(ctx, getStatesReq, getStatesResp) + + resp.Diagnostics = getStatesResp.Diagnostics + resp.StateId = getStatesResp.StateIDs + + return resp, nil +} + +func (s ProviderServer) DeleteState(ctx context.Context, req *tfprotov6.DeleteStateRequest) (*tfprotov6.DeleteStateResponse, error) { + resp := &tfprotov6.DeleteStateResponse{} + + store, diag := ProviderStateStore(s.Provider, req.TypeName) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + deleteStateReq := statestore.DeleteStateRequest{ + StateID: req.StateId, + } + deleteStateResp := &statestore.DeleteStateResponse{} + + store.DeleteState(ctx, deleteStateReq, deleteStateResp) + + resp.Diagnostics = deleteStateResp.Diagnostics + + return resp, nil +} + +func (s ProviderServer) LockState(ctx context.Context, req *tfprotov6.LockStateRequest) (*tfprotov6.LockStateResponse, error) { + resp := &tfprotov6.LockStateResponse{} + + store, diag := ProviderStateStore(s.Provider, req.TypeName) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + lockStateReq := statestore.LockStateRequest{ + StateID: req.StateId, + Operation: req.Operation, + } + lockStateResp := &statestore.LockStateResponse{} + + store.LockState(ctx, lockStateReq, lockStateResp) + + resp.Diagnostics = lockStateResp.Diagnostics + resp.LockId = lockStateResp.LockID + + return resp, nil +} + +func (s ProviderServer) UnlockState(ctx context.Context, req *tfprotov6.UnlockStateRequest) (*tfprotov6.UnlockStateResponse, error) { + resp := &tfprotov6.UnlockStateResponse{} + + store, diag := ProviderStateStore(s.Provider, req.TypeName) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + lockStateReq := statestore.UnlockStateRequest{ + StateID: req.StateId, + LockID: req.LockId, + } + lockStateResp := &statestore.UnlockStateResponse{} + + store.UnlockState(ctx, lockStateReq, lockStateResp) + + resp.Diagnostics = lockStateResp.Diagnostics + + return resp, nil +} + +func (s ProviderServer) ReadStateBytes(ctx context.Context, req *tfprotov6.ReadStateBytesRequest) (*tfprotov6.ReadStateBytesStream, error) { + resp := &tfprotov6.ReadStateBytesStream{} + + store, diag := ProviderStateStore(s.Provider, req.TypeName) + + if diag != nil { + resp.Chunks = slices.Values([]tfprotov6.ReadStateByteChunk{{Diagnostics: []*tfprotov6.Diagnostic{diag}}}) + return resp, nil + } + + readStateBytesReq := statestore.ReadStateBytesRequest{ + StateID: req.StateId, + } + readStateBytesResp := &statestore.ReadStateBytesResponse{} + + store.ReadStateBytes(ctx, readStateBytesReq, readStateBytesResp) + + if len(readStateBytesResp.Diagnostics) > 0 { + resp.Chunks = slices.Values([]tfprotov6.ReadStateByteChunk{{Diagnostics: readStateBytesResp.Diagnostics}}) + return resp, nil + } + + chunkSize := store.ConfiguredChunkSize() + reader := bytes.NewReader(readStateBytesResp.StateBytes) + totalLength := reader.Size() + rangeStart := 0 + + // TODO:PSS: Refactor -> Is there really no built-in to do this in Golang's standard library? + resp.Chunks = func(yield func(tfprotov6.ReadStateByteChunk) bool) { + for { + var diags []*tfprotov6.Diagnostic + readBytes := make([]byte, chunkSize) + byteCount, err := reader.Read(readBytes) + if err != nil && !errors.Is(err, io.EOF) { + ok := yield(tfprotov6.ReadStateByteChunk{ + StateByteChunk: tfprotov6.StateByteChunk{ + Bytes: nil, + TotalLength: 0, + Range: tfprotov6.StateByteRange{ + Start: 0, + End: 0, + }, + }, + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Error reading state", + Detail: fmt.Sprintf("An unexpected error occurred while reading state data for %s: %s", + req.StateId, + err, + ), + }, + }, + }) + if !ok { + return + } + } + + if byteCount == 0 { + // The previous iteration read the last byte of the data. + // + // We haven't used `yield` here, as yield doesn't return false.... for some reason (need to learn why). + // We need to return here to stop the iterator. + + // There is nothing to close here? + + return + } + + ok := yield(tfprotov6.ReadStateByteChunk{ + StateByteChunk: tfprotov6.StateByteChunk{ + Bytes: readBytes[0:byteCount], + TotalLength: int64(totalLength), + Range: tfprotov6.StateByteRange{ + Start: int64(rangeStart), + End: int64(rangeStart + byteCount), + }, + }, + Diagnostics: diags, + }) + if !ok { + return + } + + // Track progress to ensure Range values are correct. + rangeStart += byteCount + } + } + + return resp, nil +} + +func (s ProviderServer) WriteStateBytes(ctx context.Context, req *tfprotov6.WriteStateBytesStream) (*tfprotov6.WriteStateBytesResponse, error) { + resp := &tfprotov6.WriteStateBytesResponse{} + + // TODO:PSS: Refactor? + var buf bytes.Buffer + var chunkErr error + var typeName string + var stateId string + + for chunk := range req.Chunks { + if chunk.Err != nil { + chunkErr = chunk.Err + break + } + + // Can this be peeked using iter.Pull() ? + if chunk.Meta != nil { + typeName = chunk.Meta.TypeName + stateId = chunk.Meta.StateId + } + + _, err := buf.Write(chunk.Bytes) + if err != nil { + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Error writing state", + Detail: fmt.Sprintf("An unexpected error occurred receieving state data from Terraform: %s", err), + }) + return resp, nil + } + } + + if chunkErr != nil { + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Error writing state", + Detail: fmt.Sprintf("An unexpected GRPC error occurred receieving state data from Terraform: %s", chunkErr), + }) + return resp, nil + } + + if buf.Len() == 0 { + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Error writing state", + Detail: "No state data was received from Terraform. This is a bug and should be reported.", + }) + return resp, nil + } + + store, diag := ProviderStateStore(s.Provider, typeName) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + writeStateBytesReq := statestore.WriteStateBytesRequest{ + StateID: stateId, + StateBytes: buf.Bytes(), + } + writeStateBytesResp := &statestore.WriteStateBytesResponse{} + + store.WriteStateBytes(ctx, writeStateBytesReq, writeStateBytesResp) + + resp.Diagnostics = writeStateBytesResp.Diagnostics + + return resp, nil +} diff --git a/internal/testing/testsdk/providerserver/statestores.go b/internal/testing/testsdk/providerserver/statestores.go new file mode 100644 index 00000000..b7d18932 --- /dev/null +++ b/internal/testing/testsdk/providerserver/statestores.go @@ -0,0 +1,24 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package providerserver + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/provider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/statestore" +) + +func ProviderStateStore(p provider.Provider, typeName string) (statestore.StateStore, *tfprotov6.Diagnostic) { + s, ok := p.StateStoresMap()[typeName] + + if !ok { + return nil, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Missing State Store Type", + Detail: "The provider does not define the state store type: " + typeName, + } + } + + return s, nil +} diff --git a/internal/testing/testsdk/statestore/statestore.go b/internal/testing/testsdk/statestore/statestore.go new file mode 100644 index 00000000..f3388573 --- /dev/null +++ b/internal/testing/testsdk/statestore/statestore.go @@ -0,0 +1,102 @@ +package statestore + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +type StateStore interface { + Schema(context.Context, SchemaRequest, *SchemaResponse) + Configure(context.Context, ConfigureRequest, *ConfigureResponse) + ValidateConfig(context.Context, ValidateConfigRequest, *ValidateConfigResponse) + GetStates(context.Context, GetStatesRequest, *GetStatesResponse) + DeleteState(context.Context, DeleteStateRequest, *DeleteStateResponse) + LockState(context.Context, LockStateRequest, *LockStateResponse) + UnlockState(context.Context, UnlockStateRequest, *UnlockStateResponse) + + // For ease-of-use, the streaming of chunk data is handled in the provider server for reading/writing + ReadStateBytes(context.Context, ReadStateBytesRequest, *ReadStateBytesResponse) + WriteStateBytes(context.Context, WriteStateBytesRequest, *WriteStateBytesResponse) + + // This isn't a GRPC call, but it allows the implementation to define the chunk size while the provider server owns the actual chunking logic. + ConfiguredChunkSize() int64 +} + +type SchemaRequest struct{} + +type SchemaResponse struct { + Diagnostics []*tfprotov6.Diagnostic + Schema *tfprotov6.Schema +} + +type ConfigureRequest struct { + Config tftypes.Value + ClientCapabilities tfprotov6.StateStoreClientCapabilities +} + +type ConfigureResponse struct { + Diagnostics []*tfprotov6.Diagnostic + ServerCapabilities tfprotov6.StateStoreServerCapabilities +} + +type ValidateConfigRequest struct { + Config tftypes.Value +} + +type ValidateConfigResponse struct { + Diagnostics []*tfprotov6.Diagnostic +} + +type GetStatesRequest struct{} + +type GetStatesResponse struct { + StateIDs []string + Diagnostics []*tfprotov6.Diagnostic +} + +type DeleteStateRequest struct { + StateID string +} + +type DeleteStateResponse struct { + Diagnostics []*tfprotov6.Diagnostic +} + +type LockStateRequest struct { + StateID string + Operation string +} + +type LockStateResponse struct { + LockID string + Diagnostics []*tfprotov6.Diagnostic +} + +type UnlockStateRequest struct { + StateID string + LockID string +} + +type UnlockStateResponse struct { + Diagnostics []*tfprotov6.Diagnostic +} + +type ReadStateBytesRequest struct { + StateID string +} + +type ReadStateBytesResponse struct { + StateBytes []byte + Diagnostics []*tfprotov6.Diagnostic +} + +type WriteStateBytesRequest struct { + StateID string + StateBytes []byte +} + +type WriteStateBytesResponse struct { + Diagnostics []*tfprotov6.Diagnostic +} From a759cf52796b16d66e58f31c2e8580d12d1f35c3 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 6 Jan 2026 18:54:24 -0500 Subject: [PATCH 02/19] add workspace commands to wd --- go.mod | 6 +++-- go.sum | 4 +-- internal/plugintest/working_dir.go | 42 ++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 89cff9cc..5f193958 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/hashicorp/terraform-plugin-testing -go 1.24.0 +go 1.24.10 + +toolchain go1.24.11 require ( github.com/google/go-cmp v0.7.0 @@ -11,7 +13,7 @@ require ( github.com/hashicorp/hc-install v0.9.2 github.com/hashicorp/hcl/v2 v2.24.0 github.com/hashicorp/logutils v1.0.0 - github.com/hashicorp/terraform-exec v0.24.0 + github.com/hashicorp/terraform-exec v0.24.1-0.20260106235132-34cefc28ca08 github.com/hashicorp/terraform-json v0.27.2 github.com/hashicorp/terraform-plugin-go v0.29.1-0.20251107103451-b1dbec9688da github.com/hashicorp/terraform-plugin-log v0.10.0 diff --git a/go.sum b/go.sum index adedd846..0640f5cb 100644 --- a/go.sum +++ b/go.sum @@ -77,8 +77,8 @@ github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQx github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.24.0 h1:mL0xlk9H5g2bn0pPF6JQZk5YlByqSqrO5VoaNtAf8OE= -github.com/hashicorp/terraform-exec v0.24.0/go.mod h1:lluc/rDYfAhYdslLJQg3J0oDqo88oGQAdHR+wDqFvo4= +github.com/hashicorp/terraform-exec v0.24.1-0.20260106235132-34cefc28ca08 h1:Rz8MOZH3cAHvsgqDwMj65a1ImagpAZgzdOYn8uHsEFM= +github.com/hashicorp/terraform-exec v0.24.1-0.20260106235132-34cefc28ca08/go.mod h1:ZkcO1aTx9rwgeW5XEhdrxRKQtxhPvf6lVQvrtioo/no= github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoKST/tRDBJKU= github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= github.com/hashicorp/terraform-plugin-go v0.29.1-0.20251107103451-b1dbec9688da h1:RvFdseryCMkoBWxucUSEnQkeROpBiungf3bNQV1efgc= diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index 9083ba64..0f3e98c2 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -563,3 +563,45 @@ func (wd *WorkingDir) Query(ctx context.Context) ([]tfjson.LogMsg, error) { return messages, nil } + +func (wd *WorkingDir) Workspaces(ctx context.Context) ([]string, error) { + logging.HelperResourceTrace(ctx, "Calling Terraform CLI workspace list command") + + workspaces, _, err := wd.tf.WorkspaceList(context.Background(), tfexec.Reattach(wd.reattachInfo)) + + logging.HelperResourceTrace(ctx, "Called Terraform CLI workspace list command") + + return workspaces, err +} + +func (wd *WorkingDir) CreateWorkspace(ctx context.Context, workspace string) error { + logging.HelperResourceTrace(ctx, "Calling Terraform CLI workspace new command") + + err := wd.tf.WorkspaceNew(context.Background(), workspace, tfexec.Reattach(wd.reattachInfo)) + + logging.HelperResourceTrace(ctx, "Called Terraform CLI workspace new command") + + return err +} + +func (wd *WorkingDir) SelectWorkspace(ctx context.Context, workspace string) error { + logging.HelperResourceTrace(ctx, "Calling Terraform CLI workspace select command") + + err := wd.tf.WorkspaceSelect(context.Background(), workspace, tfexec.Reattach(wd.reattachInfo)) + + logging.HelperResourceTrace(ctx, "Called Terraform CLI workspace select command") + + return err +} + +func (wd *WorkingDir) DeleteWorkspace(ctx context.Context, workspace string, opts ...tfexec.WorkspaceDeleteCmdOption) error { + logging.HelperResourceTrace(ctx, "Calling Terraform CLI workspace delete command") + + opts = append(opts, tfexec.Reattach(wd.reattachInfo)) + + err := wd.tf.WorkspaceDelete(context.Background(), workspace, opts...) + + logging.HelperResourceTrace(ctx, "Called Terraform CLI workspace delete command") + + return err +} From c0554a341215a9e190f778aaf91bc6cef11b737b Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 8 Jan 2026 16:19:15 -0500 Subject: [PATCH 03/19] initial impl + working state store impl --- .../examplecloud_statestore_test.go | 101 ++++++ helper/resource/statestore/statestore_test.go | 190 +++++++++++ .../statestore/terraform_backend_test.go | 55 ++++ helper/resource/testing.go | 13 + helper/resource/testing_new.go | 52 +++ helper/resource/testing_new_state_store.go | 308 ++++++++++++++++++ helper/resource/teststep_validate_test.go | 7 + internal/testing/testprovider/statestore.go | 49 ++- tfversion/versions.go | 1 + 9 files changed, 751 insertions(+), 25 deletions(-) create mode 100644 helper/resource/statestore/examplecloud_statestore_test.go create mode 100644 helper/resource/statestore/statestore_test.go create mode 100644 helper/resource/statestore/terraform_backend_test.go create mode 100644 helper/resource/testing_new_state_store.go diff --git a/helper/resource/statestore/examplecloud_statestore_test.go b/helper/resource/statestore/examplecloud_statestore_test.go new file mode 100644 index 00000000..294f017b --- /dev/null +++ b/helper/resource/statestore/examplecloud_statestore_test.go @@ -0,0 +1,101 @@ +package statestore_test + +import ( + "context" + "errors" + "fmt" + "io" + "io/fs" + "path/filepath" + "strings" + "testing/fstest" + "time" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/internal/logging" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/statestore" +) + +// This state store implementation uses an in-memory map for storing state in a filesystem-like manner. +// This only works because the terraform-plugin-testing harness keeps provider instances running +// throughout a Go test with multiple Terraform CLI command calls by using a reattach configuration. +func exampleCloudStateStoreFS() *testprovider.StateStore { + memFS := fstest.MapFS{} + + return &testprovider.StateStore{ + SchemaResponse: &statestore.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{Attributes: []*tfprotov6.SchemaAttribute{}}, + }, + }, + GetStatesFunc: func(ctx context.Context, req statestore.GetStatesRequest, resp *statestore.GetStatesResponse) { + directories, err := memFS.ReadDir(".") + if err != nil { + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Error reading inmem filesystem", + Detail: err.Error(), + }) + return + } + + workspaces := make([]string, 0) + for _, dir := range directories { + workspaces = append(workspaces, dir.Name()) + } + + resp.StateIDs = workspaces + }, + DeleteStateFunc: func(ctx context.Context, req statestore.DeleteStateRequest, resp *statestore.DeleteStateResponse) { + for filePath := range memFS { + if strings.HasPrefix(filePath, req.StateID) { + delete(memFS, filePath) + } + } + }, + LockStateFunc: func(ctx context.Context, req statestore.LockStateRequest, resp *statestore.LockStateResponse) { + logging.HelperResourceDebug(ctx, "examplecloud_inmem: Lock support not implemented") + }, + UnlockStateFunc: func(ctx context.Context, req statestore.UnlockStateRequest, resp *statestore.UnlockStateResponse) { + logging.HelperResourceDebug(ctx, "examplecloud_inmem: Lock support not implemented") + }, + ReadStateBytesFunc: func(ctx context.Context, req statestore.ReadStateBytesRequest, resp *statestore.ReadStateBytesResponse) { + stateFilePath := filepath.Join(req.StateID, "terraform.tfstate") + stateFile, err := memFS.Open(stateFilePath) + if err != nil { + // If there is no state file, Terraform will create one. + if errors.Is(err, fs.ErrNotExist) { + return + } + + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: fmt.Sprintf("Error state %q at path %q", req.StateID, stateFilePath), + Detail: err.Error(), + }) + return + } + + stateBytes, err := io.ReadAll(stateFile) + if err != nil { + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: fmt.Sprintf("Error reading %q state bytes", req.StateID), + Detail: err.Error(), + }) + return + } + + resp.StateBytes = stateBytes + }, + WriteStateBytesFunc: func(ctx context.Context, req statestore.WriteStateBytesRequest, resp *statestore.WriteStateBytesResponse) { + stateFilePath := filepath.Join(req.StateID, "terraform.tfstate") + memFS[stateFilePath] = &fstest.MapFile{ + Data: req.StateBytes, + Mode: fs.ModePerm, + ModTime: time.Now(), + } + }, + } +} diff --git a/helper/resource/statestore/statestore_test.go b/helper/resource/statestore/statestore_test.go new file mode 100644 index 00000000..8fd41b7f --- /dev/null +++ b/helper/resource/statestore/statestore_test.go @@ -0,0 +1,190 @@ +package statestore_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/statestore" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestStateStore_validation_error(t *testing.T) { + // Setting this environment variable ensures TF core uses pluggable state storage during init. + // This is only temporary while PSS is experimental. + t.Setenv("TF_ENABLE_PLUGGABLE_STATE_STORAGE", "1") + + r.UnitTest(t, r.TestCase{ + // State stores currently are only available in alpha releases or built from source + // with experiments enabled: `go install -ldflags="-X main.experimentsAllowed=yes" .` + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_15_0), + tfversion.SkipIfNotPrerelease(), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + StateStores: map[string]*testprovider.StateStore{ + "examplecloud_inmem": { + SchemaResponse: &statestore.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "path", + Type: tftypes.String, + Required: true, + }, + }, + }, + }, + }, + ValidateConfigResponse: &statestore.ValidateConfigResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "WHOOPS", + Detail: "Something isn't right about that request :D, error it is!", + Attribute: tftypes.NewAttributePath().WithAttributeName("path"), + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + StateStore: true, + Config: ` + terraform { + required_providers { + examplecloud = { + source = "registry.terraform.io/hashicorp/examplecloud" + } + } + state_store "examplecloud_inmem" { + provider "examplecloud" {} + + path = "test_state_file.tfstate" + } + } + `, + ExpectError: regexp.MustCompile(`Something isn't right about that request :D, error it is!`), + }, + }, + }) +} + +func TestStateStore_configure_error(t *testing.T) { + // Setting this environment variable ensures TF core uses pluggable state storage during init. + // This is only temporary while PSS is experimental. + t.Setenv("TF_ENABLE_PLUGGABLE_STATE_STORAGE", "1") + + r.UnitTest(t, r.TestCase{ + // State stores currently are only available in alpha releases or built from source + // with experiments enabled: `go install -ldflags="-X main.experimentsAllowed=yes" .` + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_15_0), + tfversion.SkipIfNotPrerelease(), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + StateStores: map[string]*testprovider.StateStore{ + "examplecloud_inmem": { + SchemaResponse: &statestore.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{Attributes: []*tfprotov6.SchemaAttribute{}}, + }, + }, + ConfigureResponse: &statestore.ConfigureResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "WHOOPS", + Detail: "The configure has failed us! :P", + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + StateStore: true, + Config: ` + terraform { + required_providers { + examplecloud = { + source = "registry.terraform.io/hashicorp/examplecloud" + } + } + state_store "examplecloud_inmem" { + provider "examplecloud" {} + } + } + `, + ExpectError: regexp.MustCompile(`The configure has failed us! :P`), + }, + }, + }) +} + +func TestStateStore_inmem(t *testing.T) { + // Setting this environment variable ensures TF core uses pluggable state storage during init. + // This is only temporary while PSS is experimental. + t.Setenv("TF_ENABLE_PLUGGABLE_STATE_STORAGE", "1") + + r.UnitTest(t, r.TestCase{ + // State stores currently are only available in alpha releases or built from source + // with experiments enabled: `go install -ldflags="-X main.experimentsAllowed=yes" .` + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_15_0), + tfversion.SkipIfNotPrerelease(), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + StateStores: map[string]*testprovider.StateStore{ + "examplecloud_inmem": exampleCloudStateStoreFS(), + }, + }), + }, + Steps: []r.TestStep{ + { + StateStore: true, + Config: ` + terraform { + required_providers { + examplecloud = { + source = "registry.terraform.io/hashicorp/examplecloud" + } + } + state_store "examplecloud_inmem" { + provider "examplecloud" {} + } + } + `, + }, + }, + }) +} + +// What we can test so far w/ smoke test: +// - State store validation +// - State store configuration +// - State store can write state +// - State store can read state +// - State store handles multiple workspaces + +// What is kind of awkward to test w/ smoke test +// - State size / chunking. You can do this by just changing the config you pass in w/ StateStore (we always inject a single resource to verify state apply will work) +// - Multiple applies to the same state (currently it just does one apply to the bar workspace) + +// What we can't test w/ smoke test +// - State store locks +// - State store unlocks +// - State store locks with a ton of clients (i.e. load test) diff --git a/helper/resource/statestore/terraform_backend_test.go b/helper/resource/statestore/terraform_backend_test.go new file mode 100644 index 00000000..0a4e1a59 --- /dev/null +++ b/helper/resource/statestore/terraform_backend_test.go @@ -0,0 +1,55 @@ +package statestore_test + +import ( + "regexp" + "testing" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +// MAINTAINER NOTE: While the StateStore mode is designed to test state store implementations, it can +// also be used to test existing Terraform core backends, which we do in this test file just for +// additional verification. + +func TestTerraformBackend_local_empty_path_validation_error(t *testing.T) { + r.UnitTest(t, r.TestCase{ + // MAINTAINER NOTE: Test steps won't run without a provider definition, so this is just + // needed to pass validation, as we're just testing Terraform core itself. + ExternalProviders: map[string]r.ExternalProvider{ + "terraform": {Source: "terraform.io/builtin/terraform"}, + }, + Steps: []r.TestStep{ + { + StateStore: true, + Config: ` + terraform { + backend "local" { + path = "" + } + } + `, + ExpectError: regexp.MustCompile(`The "path" attribute value must not be empty.`), + }, + }, + }) +} + +func TestTerraformBackend_local(t *testing.T) { + r.UnitTest(t, r.TestCase{ + // MAINTAINER NOTE: Test steps won't run without a provider definition, so this is just + // needed to pass validation, as we're just testing Terraform core itself. + ExternalProviders: map[string]r.ExternalProvider{ + "terraform": {Source: "terraform.io/builtin/terraform"}, + }, + Steps: []r.TestStep{ + { + StateStore: true, + Config: ` + terraform { + backend "local" {} + } + `, + }, + }, + }) +} diff --git a/helper/resource/testing.go b/helper/resource/testing.go index a9b21c24..b3fdc02c 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -848,6 +848,19 @@ type TestStep struct { // If true, the test step will run the query command Query bool + + // The StateStore mode is used for testing state store implementations in a provider. The StateStore mode runs + // various Terraform CLI commands to ensure the configured state store operates in a manner expected by Terraform core. + // + // The StateStore mode expects state_store configuration to be provided using one of the Config, ConfigFile, + // or ConfigDirectory fields. + // + // TODO:PSS: List out all of the commands / assertions that are made + // + // TODO:PSS: Document what will be created/cleaned up in backend, mention the provider developer is responsible for setting/cleaning up + // the backend itself during test, but the testing mode should leave no artifacts in backend (all workspace modifications are deleted, + // default workspace is untouched). + StateStore bool } // ConfigPlanChecks defines the different points in a Config TestStep when plan checks can be run. diff --git a/helper/resource/testing_new.go b/helper/resource/testing_new.go index 13e60001..78faa43f 100644 --- a/helper/resource/testing_new.go +++ b/helper/resource/testing_new.go @@ -62,9 +62,19 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest protov6: c.ProtoV6ProviderFactories, } + // TODO:PSS: Should revisit this and see if it's necessary + // If any of the test steps used the StateStore mode and tested an error, make sure we don't execute any more commands with an invalid state store + var initializationErrorOccurred bool + defer func() { t.Helper() + // We can't retrieve the state because the backend/state store isn't fully initialized. + if initializationErrorOccurred { + wd.Close() + return + } + var statePreDestroy *terraform.State var err error err = runProviderCommand(ctx, t, wd, providers, func() error { @@ -394,6 +404,48 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest continue } + if step.StateStore { + logging.HelperResourceTrace(ctx, "TestStep is StateStore mode") + + err := testStepNewStateStore(ctx, t, wd, step, providers, cfg) + if err != nil { + // Ensure the TestStep doesn't run any Terraform commands that expect the backend/state store to be initialized + initializationErrorOccurred = true + } + + // Check if the error + if step.ExpectError != nil { + logging.HelperResourceDebug(ctx, "Checking TestStep ExpectError") + if err == nil { + logging.HelperResourceError(ctx, "Error running state store tests: expected an error but got none") + t.Fatalf("Step %d/%d error running state store tests: expected an error but got none", stepNumber, len(c.Steps)) + } + + if !step.ExpectError.MatchString(err.Error()) { + logging.HelperResourceError(ctx, fmt.Sprintf("Error running state store tests: expected an error with pattern (%s)", step.ExpectError.String()), + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Step %d/%d error running state store tests, 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 state store tests", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Step %d/%d error running state store tests: %s", stepNumber, len(c.Steps), err) + } + } + + logging.HelperResourceDebug(ctx, "Finished TestStep") + + continue + } + if cfg != nil { logging.HelperResourceTrace(ctx, "TestStep is Config mode") diff --git a/helper/resource/testing_new_state_store.go b/helper/resource/testing_new_state_store.go new file mode 100644 index 00000000..c84efbf4 --- /dev/null +++ b/helper/resource/testing_new_state_store.go @@ -0,0 +1,308 @@ +package resource + +import ( + "context" + "errors" + "fmt" + "slices" + + "github.com/hashicorp/terraform-exec/tfexec" + tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/mitchellh/go-testing-interface" +) + +// TODO:PSS: I should go back through and review this logic, refactor, maybe add/remove any details we don't need +// It's likely we will add lock testing somewhere here as well, so might be worth refactoring at the top-level +// +// 1. StateStore (true) + nothing else == Run basic smoke test +// 2. StateStore (true) + VerifyLock (true) == Run concurrent lock test +// 2. StateStore (true) + VerifyLock (true) + ForceUnlock (true) == Run concurrent lock test, then finish with a force unlock (maybe do this by default?) +// 2. StateStore (true) + ??? == Run lock soak test (configurable) +func testStepNewStateStore(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, step TestStep, providers *providerFactories, cfg teststep.Config) error { + t.Helper() + + err := wd.SetConfig(ctx, cfg, step.ConfigVariables) + if err != nil { + return fmt.Errorf("Error setting config: %w", err) + } + + // ----- Validate and configure the state store + err = runProviderCommand(ctx, t, wd, providers, func() error { + return wd.Init(ctx) + }) + if err != nil { + return fmt.Errorf("Error running init: %w", err) + } + + // ----- Retrieve all the workspaces + workspaces := make([]string, 0) + err = runProviderCommand(ctx, t, wd, providers, func() error { + workspaces, err = wd.Workspaces(ctx) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return fmt.Errorf("Error getting workspaces: %w", err) + } + + // Assert the only workspace created is the "default" one + // TODO:PSS: Might need to revisit this assertion for state stores. Not sure what the behavior will be during init, is this expected still? + // TODO:PSS: Is it possible to not support this for state stores or TF core backends? The cloud backend doesn't but that one is special :P + if len(workspaces) != 1 || workspaces[0] != "default" { + t.Fatalf("Expected a single workspace named \"default\" after initialization, got: %#v", workspaces) + } + + // ----- Create "foo" workspace and assert the state returned is empty + err = runProviderCommand(ctx, t, wd, providers, func() error { + return wd.CreateWorkspace(ctx, "foo") + }) + if err != nil { + return fmt.Errorf("Error creating \"foo\" workspace: %w", err) + } + + var fooState *tfjson.State + err = runProviderCommand(ctx, t, wd, providers, func() error { + fooState, err = wd.State(ctx) + return err + }) + if err != nil { + return fmt.Errorf("Error retrieving \"foo\" state: %w", err) + } + + if fooState.Values != nil && fooState.Values.RootModule != nil && len(fooState.Values.RootModule.Resources) > 0 { + t.Fatalf("Expected the newly created \"foo\" state to be empty. Found %d resources.", len(fooState.Values.RootModule.Resources)) + } + + // ----- Create "bar" workspace and assert the state returned is empty + err = runProviderCommand(ctx, t, wd, providers, func() error { + return wd.CreateWorkspace(ctx, "bar") + }) + if err != nil { + return fmt.Errorf("Error creating \"bar\" workspace: %w", err) + } + + var barState *tfjson.State + err = runProviderCommand(ctx, t, wd, providers, func() error { + barState, err = wd.State(ctx) + return err + }) + if err != nil { + return fmt.Errorf("Error retrieving \"bar\" state: %w", err) + } + + if barState.Values != nil && barState.Values.RootModule != nil && len(barState.Values.RootModule.Resources) > 0 { + t.Fatalf("Expected the newly created \"bar\" state to be empty. Found %d resources.", len(barState.Values.RootModule.Resources)) + } + + // ----- Add a fake resource to the bar workspace + barConfig := ` +resource "terraform_data" "tf_plugin_testing_resource_bar" { + input = "this resource was injected by terraform-plugin-testing" +}` + + // TODO:PSS: I'm 99% sure this should work with all of config file/directory implementations + cfgWithBar := cfg.Append(barConfig) + err = wd.SetConfig(ctx, cfgWithBar, step.ConfigVariables) + if err != nil { + return fmt.Errorf("Error setting config: %w", err) + } + + // ----- Apply bar workspace + err = runProviderCommand(ctx, t, wd, providers, func() error { + return wd.Apply(ctx) + }) + if err != nil { + return fmt.Errorf("Error creating fake resource in \"bar\" workspace: %w", err) + } + + // ----- Grab the bar state again + err = runProviderCommand(ctx, t, wd, providers, func() error { + barState, err = wd.State(ctx) + return err + }) + if err != nil { + return fmt.Errorf("Error retrieving \"bar\" state: %w", err) + } + + // ----- Check if the resource exists in the "bar" state + // TODO:PSS: Should we not use a statecheck here? Could just read the state manually... + checkOutput := statecheck.ExpectKnownValue( + "terraform_data.tf_plugin_testing_resource_bar", + tfjsonpath.New("output"), + knownvalue.StringExact("this resource was injected by terraform-plugin-testing"), + ) + + checkResp := statecheck.CheckStateResponse{} + checkOutput.CheckState(ctx, statecheck.CheckStateRequest{State: barState}, &checkResp) + + if checkResp.Error != nil { + return fmt.Errorf("After writing a test resource instance object to \"bar\" and re-reading it, the object has vanished: %w", err) + } + + // ----- Switch to the "foo" workspace and grab the state, ensuring it's still empty. + err = runProviderCommand(ctx, t, wd, providers, func() error { + return wd.SelectWorkspace(ctx, "foo") + }) + if err != nil { + return fmt.Errorf("Error creating \"foo\" workspace: %w", err) + } + + err = runProviderCommand(ctx, t, wd, providers, func() error { + fooState, err = wd.State(ctx) + return err + }) + if err != nil { + return fmt.Errorf("Error retrieving \"foo\" state: %w", err) + } + + if fooState.Values != nil && fooState.Values.RootModule != nil && len(fooState.Values.RootModule.Resources) > 0 { + t.Fatalf("After writing a resource to \"bar\" state, expected the \"foo\" state to be empty. Found %d resources in \"foo\".", len(fooState.Values.RootModule.Resources)) + } + + // ----- Verify when we list the workspaces we get back "default", "foo" (created during this test), and "bar" (created during this test). + workspaces = make([]string, 0) + err = runProviderCommand(ctx, t, wd, providers, func() error { + workspaces, err = wd.Workspaces(ctx) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return fmt.Errorf("Error getting workspaces: %w", err) + } + + slices.Sort(workspaces) + expected := []string{"bar", "default", "foo"} + if !slices.Equal(expected, workspaces) { + t.Fatalf("Expected workspaces to be %#v, got: %#v", expected, workspaces) + } + + // ----- Delete "bar" workspace + + // Switch to "foo" workspace so we can delete bar. + err = runProviderCommand(ctx, t, wd, providers, func() error { + return wd.SelectWorkspace(ctx, "foo") + }) + if err != nil { + return fmt.Errorf("Error selecting \"foo\" workspace: %w", err) + } + err = runProviderCommand(ctx, t, wd, providers, func() error { + return wd.DeleteWorkspace(ctx, "bar", tfexec.Force(true)) + }) + if err != nil { + return fmt.Errorf("Error deleting \"bar\" workspace: %w", err) + } + + // ----- Attempt to delete "default" workspace, assert error + err = runProviderCommand(ctx, t, wd, providers, func() error { + return wd.DeleteWorkspace(ctx, "default", tfexec.Force(true)) + }) + if err == nil { + return errors.New("Expected error when deleting \"default\" workspace") + } + + // ----- Recreate the "bar" workspace + err = runProviderCommand(ctx, t, wd, providers, func() error { + return wd.CreateWorkspace(ctx, "bar") + }) + if err != nil { + return fmt.Errorf("Error creating \"bar\" workspace: %w", err) + } + + // ----- Grab "bar" state and assert it is empty (i.e. no left over artifacts) + err = runProviderCommand(ctx, t, wd, providers, func() error { + barState, err = wd.State(ctx) + return err + }) + if err != nil { + return fmt.Errorf("Error retrieving \"bar\" state: %w", err) + } + + if barState.Values != nil && barState.Values.RootModule != nil && len(barState.Values.RootModule.Resources) > 0 { + t.Fatalf("Expected the newly created \"bar\" state to be empty. Found %d resources.", len(barState.Values.RootModule.Resources)) + } + + // ----- Delete "bar" workspace again, force=true + + // Switch to "foo" workspace so we can delete bar. + err = runProviderCommand(ctx, t, wd, providers, func() error { + return wd.SelectWorkspace(ctx, "foo") + }) + if err != nil { + return fmt.Errorf("Error selecting \"foo\" workspace: %w", err) + } + err = runProviderCommand(ctx, t, wd, providers, func() error { + return wd.DeleteWorkspace(ctx, "bar", tfexec.Force(true)) + }) + if err != nil { + return fmt.Errorf("Error deleting \"bar\" workspace: %w", err) + } + + // ----- List workspaces and verify it's just "foo" and "default" + workspaces = make([]string, 0) + err = runProviderCommand(ctx, t, wd, providers, func() error { + workspaces, err = wd.Workspaces(ctx) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return fmt.Errorf("Error getting workspaces: %w", err) + } + + slices.Sort(workspaces) + expected = []string{"default", "foo"} + if !slices.Equal(expected, workspaces) { + t.Fatalf("Expected workspaces to be %#v, got: %#v", expected, workspaces) + } + + // ----- Delete "foo" workspace, force=true + + // Switch to "default" workspace so we can delete bar. + err = runProviderCommand(ctx, t, wd, providers, func() error { + return wd.SelectWorkspace(ctx, "default") + }) + if err != nil { + return fmt.Errorf("Error selecting \"default\" workspace: %w", err) + } + err = runProviderCommand(ctx, t, wd, providers, func() error { + return wd.DeleteWorkspace(ctx, "foo", tfexec.Force(true)) + }) + if err != nil { + return fmt.Errorf("Error deleting \"foo\" workspace: %w", err) + } + + // ----- List workspaces and verify it's just "default" (which we did not modify) + workspaces = make([]string, 0) + err = runProviderCommand(ctx, t, wd, providers, func() error { + workspaces, err = wd.Workspaces(ctx) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return fmt.Errorf("Error getting workspaces: %w", err) + } + + slices.Sort(workspaces) + expected = []string{"default"} + if !slices.Equal(expected, workspaces) { + t.Fatalf("Expected workspaces to be %#v, got: %#v", expected, workspaces) + } + + return nil +} diff --git a/helper/resource/teststep_validate_test.go b/helper/resource/teststep_validate_test.go index f94f1a4a..fd66d9b8 100644 --- a/helper/resource/teststep_validate_test.go +++ b/helper/resource/teststep_validate_test.go @@ -503,6 +503,13 @@ func TestTestStepValidate(t *testing.T) { testStepValidateRequest: testStepValidateRequest{TestCaseHasProviders: true}, expectedError: errors.New("TestStep RefreshPlanChecks.PostRefresh must only be specified with RefreshState"), }, + "state-store-mode-missing-config": { + testStep: TestStep{ + StateStore: true, + }, + testStepValidateRequest: testStepValidateRequest{}, + expectedError: fmt.Errorf("TestStep missing Config or ConfigDirectory or ConfigFile or ImportState or RefreshState"), + }, } for name, test := range tests { diff --git a/internal/testing/testprovider/statestore.go b/internal/testing/testprovider/statestore.go index fb895c5a..a9e54118 100644 --- a/internal/testing/testprovider/statestore.go +++ b/internal/testing/testprovider/statestore.go @@ -14,15 +14,18 @@ var _ statestore.StateStore = &StateStore{} type StateStore struct { configuredChunkSize int64 - SchemaResponse *statestore.SchemaResponse - ConfigureResponse *statestore.ConfigureResponse - ValidateConfigResponse *statestore.ValidateConfigResponse - GetStatesResponse *statestore.GetStatesResponse - DeleteStateResponse *statestore.DeleteStateResponse - LockStateResponse *statestore.LockStateResponse - UnlockStateResponse *statestore.UnlockStateResponse - ReadStateBytesResponse *statestore.ReadStateBytesResponse - WriteStateBytesResponse *statestore.WriteStateBytesResponse + SchemaResponse *statestore.SchemaResponse + ConfigureResponse *statestore.ConfigureResponse + ValidateConfigResponse *statestore.ValidateConfigResponse + + // Declaring a mock state store is slightly more complicated then other types since the implementation is likely + // needed to be stateful to work with multiple terraform commands. + GetStatesFunc func(context.Context, statestore.GetStatesRequest, *statestore.GetStatesResponse) + DeleteStateFunc func(context.Context, statestore.DeleteStateRequest, *statestore.DeleteStateResponse) + LockStateFunc func(context.Context, statestore.LockStateRequest, *statestore.LockStateResponse) + UnlockStateFunc func(context.Context, statestore.UnlockStateRequest, *statestore.UnlockStateResponse) + ReadStateBytesFunc func(context.Context, statestore.ReadStateBytesRequest, *statestore.ReadStateBytesResponse) + WriteStateBytesFunc func(context.Context, statestore.WriteStateBytesRequest, *statestore.WriteStateBytesResponse) } func (s *StateStore) Schema(ctx context.Context, req statestore.SchemaRequest, resp *statestore.SchemaResponse) { @@ -52,42 +55,38 @@ func (s *StateStore) ValidateConfig(ctx context.Context, req statestore.Validate } } -// TODO:PSS: Probably need to adjust some of these to be callback functions, rather then field responses. func (s *StateStore) GetStates(ctx context.Context, req statestore.GetStatesRequest, resp *statestore.GetStatesResponse) { - if s.GetStatesResponse != nil { - resp.Diagnostics = s.GetStatesResponse.Diagnostics - resp.StateIDs = s.GetStatesResponse.StateIDs + if s.GetStatesFunc != nil { + s.GetStatesFunc(ctx, req, resp) } } func (s *StateStore) DeleteState(ctx context.Context, req statestore.DeleteStateRequest, resp *statestore.DeleteStateResponse) { - if s.DeleteStateResponse != nil { - resp.Diagnostics = s.DeleteStateResponse.Diagnostics + if s.DeleteStateFunc != nil { + s.DeleteStateFunc(ctx, req, resp) } } func (s *StateStore) LockState(ctx context.Context, req statestore.LockStateRequest, resp *statestore.LockStateResponse) { - if s.LockStateResponse != nil { - resp.LockID = s.LockStateResponse.LockID - resp.Diagnostics = s.LockStateResponse.Diagnostics + if s.LockStateFunc != nil { + s.LockStateFunc(ctx, req, resp) } } func (s *StateStore) UnlockState(ctx context.Context, req statestore.UnlockStateRequest, resp *statestore.UnlockStateResponse) { - if s.UnlockStateResponse != nil { - resp.Diagnostics = s.UnlockStateResponse.Diagnostics + if s.UnlockStateFunc != nil { + s.UnlockStateFunc(ctx, req, resp) } } func (s *StateStore) ReadStateBytes(ctx context.Context, req statestore.ReadStateBytesRequest, resp *statestore.ReadStateBytesResponse) { - if s.ReadStateBytesResponse != nil { - resp.Diagnostics = s.ReadStateBytesResponse.Diagnostics - resp.StateBytes = s.ReadStateBytesResponse.StateBytes + if s.ReadStateBytesFunc != nil { + s.ReadStateBytesFunc(ctx, req, resp) } } func (s *StateStore) WriteStateBytes(ctx context.Context, req statestore.WriteStateBytesRequest, resp *statestore.WriteStateBytesResponse) { - if s.WriteStateBytesResponse != nil { - resp.Diagnostics = s.WriteStateBytesResponse.Diagnostics + if s.WriteStateBytesFunc != nil { + s.WriteStateBytesFunc(ctx, req, resp) } } diff --git a/tfversion/versions.go b/tfversion/versions.go index 55c0b12c..ee204fad 100644 --- a/tfversion/versions.go +++ b/tfversion/versions.go @@ -41,4 +41,5 @@ var ( Version1_12_0 *version.Version = version.Must(version.NewVersion("1.12.0")) Version1_13_0 *version.Version = version.Must(version.NewVersion("1.13.0")) Version1_14_0 *version.Version = version.Must(version.NewVersion("1.14.0")) + Version1_15_0 *version.Version = version.Must(version.NewVersion("1.15.0")) ) From 8d91013bb686fd6bd330170896e49989d24320d2 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 9 Jan 2026 10:58:46 -0500 Subject: [PATCH 04/19] refactoring state store mode and add new tests --- .../examplecloud_statestore_test.go | 2 +- helper/resource/statestore/statestore_test.go | 92 ++++- .../statestore/terraform_backend_test.go | 14 +- helper/resource/testing.go | 9 +- helper/resource/testing_new.go | 1 - helper/resource/testing_new_state_store.go | 316 +++++++++--------- 6 files changed, 251 insertions(+), 183 deletions(-) diff --git a/helper/resource/statestore/examplecloud_statestore_test.go b/helper/resource/statestore/examplecloud_statestore_test.go index 294f017b..962bea42 100644 --- a/helper/resource/statestore/examplecloud_statestore_test.go +++ b/helper/resource/statestore/examplecloud_statestore_test.go @@ -20,7 +20,7 @@ import ( // This state store implementation uses an in-memory map for storing state in a filesystem-like manner. // This only works because the terraform-plugin-testing harness keeps provider instances running // throughout a Go test with multiple Terraform CLI command calls by using a reattach configuration. -func exampleCloudStateStoreFS() *testprovider.StateStore { +func exampleCloudValidStateStore() *testprovider.StateStore { memFS := fstest.MapFS{} return &testprovider.StateStore{ diff --git a/helper/resource/statestore/statestore_test.go b/helper/resource/statestore/statestore_test.go index 8fd41b7f..f038ba3a 100644 --- a/helper/resource/statestore/statestore_test.go +++ b/helper/resource/statestore/statestore_test.go @@ -13,6 +13,45 @@ import ( "github.com/hashicorp/terraform-plugin-testing/tfversion" ) +func TestStateStore_inmem_no_lock(t *testing.T) { + // Setting this environment variable ensures TF core uses pluggable state storage during init. + // This is only temporary while PSS is experimental. + t.Setenv("TF_ENABLE_PLUGGABLE_STATE_STORAGE", "1") + + r.UnitTest(t, r.TestCase{ + // State stores currently are only available in alpha releases or built from source + // with experiments enabled: `go install -ldflags="-X main.experimentsAllowed=yes" .` + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_15_0), + tfversion.SkipIfNotPrerelease(), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + StateStores: map[string]*testprovider.StateStore{ + "examplecloud_inmem": exampleCloudValidStateStore(), + }, + }), + }, + Steps: []r.TestStep{ + { + StateStore: true, + Config: ` + terraform { + required_providers { + examplecloud = { + source = "registry.terraform.io/hashicorp/examplecloud" + } + } + state_store "examplecloud_inmem" { + provider "examplecloud" {} + } + } + `, + }, + }, + }) +} + func TestStateStore_validation_error(t *testing.T) { // Setting this environment variable ensures TF core uses pluggable state storage during init. // This is only temporary while PSS is experimental. @@ -134,11 +173,59 @@ func TestStateStore_configure_error(t *testing.T) { }) } -func TestStateStore_inmem(t *testing.T) { +func TestStateStore_workspace_delete_error(t *testing.T) { + // Setting this environment variable ensures TF core uses pluggable state storage during init. + // This is only temporary while PSS is experimental. + t.Setenv("TF_ENABLE_PLUGGABLE_STATE_STORAGE", "1") + + // Simulating an invalid state store that doesn't support deleting workspaces + stateStoreImpl := exampleCloudValidStateStore() + stateStoreImpl.DeleteStateFunc = nil + + r.UnitTest(t, r.TestCase{ + // State stores currently are only available in alpha releases or built from source + // with experiments enabled: `go install -ldflags="-X main.experimentsAllowed=yes" .` + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_15_0), + tfversion.SkipIfNotPrerelease(), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + StateStores: map[string]*testprovider.StateStore{ + "examplecloud_inmem": stateStoreImpl, + }, + }), + }, + Steps: []r.TestStep{ + { + StateStore: true, + Config: ` + terraform { + required_providers { + examplecloud = { + source = "registry.terraform.io/hashicorp/examplecloud" + } + } + state_store "examplecloud_inmem" { + provider "examplecloud" {} + } + } + `, + ExpectError: regexp.MustCompile(`Workspace "bar" already exists`), + }, + }, + }) +} + +func TestStateStore_invalid_write_state(t *testing.T) { // Setting this environment variable ensures TF core uses pluggable state storage during init. // This is only temporary while PSS is experimental. t.Setenv("TF_ENABLE_PLUGGABLE_STATE_STORAGE", "1") + // Simulating an invalid state store that doesn't support writing state + stateStoreImpl := exampleCloudValidStateStore() + stateStoreImpl.WriteStateBytesFunc = nil + r.UnitTest(t, r.TestCase{ // State stores currently are only available in alpha releases or built from source // with experiments enabled: `go install -ldflags="-X main.experimentsAllowed=yes" .` @@ -149,7 +236,7 @@ func TestStateStore_inmem(t *testing.T) { ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ StateStores: map[string]*testprovider.StateStore{ - "examplecloud_inmem": exampleCloudStateStoreFS(), + "examplecloud_inmem": stateStoreImpl, }, }), }, @@ -168,6 +255,7 @@ func TestStateStore_inmem(t *testing.T) { } } `, + ExpectError: regexp.MustCompile(`After init, expected the "default" workspace to be created`), }, }, }) diff --git a/helper/resource/statestore/terraform_backend_test.go b/helper/resource/statestore/terraform_backend_test.go index 0a4e1a59..3ba3c01d 100644 --- a/helper/resource/statestore/terraform_backend_test.go +++ b/helper/resource/statestore/terraform_backend_test.go @@ -11,7 +11,7 @@ import ( // also be used to test existing Terraform core backends, which we do in this test file just for // additional verification. -func TestTerraformBackend_local_empty_path_validation_error(t *testing.T) { +func TestTerraformBackend_local(t *testing.T) { r.UnitTest(t, r.TestCase{ // MAINTAINER NOTE: Test steps won't run without a provider definition, so this is just // needed to pass validation, as we're just testing Terraform core itself. @@ -23,18 +23,15 @@ func TestTerraformBackend_local_empty_path_validation_error(t *testing.T) { StateStore: true, Config: ` terraform { - backend "local" { - path = "" - } + backend "local" {} } `, - ExpectError: regexp.MustCompile(`The "path" attribute value must not be empty.`), }, }, }) } -func TestTerraformBackend_local(t *testing.T) { +func TestTerraformBackend_local_empty_path_validation_error(t *testing.T) { r.UnitTest(t, r.TestCase{ // MAINTAINER NOTE: Test steps won't run without a provider definition, so this is just // needed to pass validation, as we're just testing Terraform core itself. @@ -46,9 +43,12 @@ func TestTerraformBackend_local(t *testing.T) { StateStore: true, Config: ` terraform { - backend "local" {} + backend "local" { + path = "" + } } `, + ExpectError: regexp.MustCompile(`The "path" attribute value must not be empty.`), }, }, }) diff --git a/helper/resource/testing.go b/helper/resource/testing.go index b3fdc02c..00307e01 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -855,11 +855,10 @@ type TestStep struct { // The StateStore mode expects state_store configuration to be provided using one of the Config, ConfigFile, // or ConfigDirectory fields. // - // TODO:PSS: List out all of the commands / assertions that are made - // - // TODO:PSS: Document what will be created/cleaned up in backend, mention the provider developer is responsible for setting/cleaning up - // the backend itself during test, but the testing mode should leave no artifacts in backend (all workspace modifications are deleted, - // default workspace is untouched). + // StateStore mode tests that the provided state store: + // - Can be successfully initialized (validation and configuring) + // - Can read and write state + // - Supports workspaces (creating and deleting) StateStore bool } diff --git a/helper/resource/testing_new.go b/helper/resource/testing_new.go index 78faa43f..0322f714 100644 --- a/helper/resource/testing_new.go +++ b/helper/resource/testing_new.go @@ -62,7 +62,6 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest protov6: c.ProtoV6ProviderFactories, } - // TODO:PSS: Should revisit this and see if it's necessary // If any of the test steps used the StateStore mode and tested an error, make sure we don't execute any more commands with an invalid state store var initializationErrorOccurred bool diff --git a/helper/resource/testing_new_state_store.go b/helper/resource/testing_new_state_store.go index c84efbf4..c6500b19 100644 --- a/helper/resource/testing_new_state_store.go +++ b/helper/resource/testing_new_state_store.go @@ -16,13 +16,10 @@ import ( "github.com/mitchellh/go-testing-interface" ) -// TODO:PSS: I should go back through and review this logic, refactor, maybe add/remove any details we don't need -// It's likely we will add lock testing somewhere here as well, so might be worth refactoring at the top-level -// -// 1. StateStore (true) + nothing else == Run basic smoke test -// 2. StateStore (true) + VerifyLock (true) == Run concurrent lock test -// 2. StateStore (true) + VerifyLock (true) + ForceUnlock (true) == Run concurrent lock test, then finish with a force unlock (maybe do this by default?) -// 2. StateStore (true) + ??? == Run lock soak test (configurable) +// testStepNewStateStore will run a series of Terraform commands with the goal of ensuring that the state store (defined in config): +// - Can be successfully initialized (validation and configuring) +// - Can read and write state +// - Supports workspaces (creating and deleting) func testStepNewStateStore(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, step TestStep, providers *providerFactories, cfg teststep.Config) error { t.Helper() @@ -31,7 +28,7 @@ func testStepNewStateStore(ctx context.Context, t testing.T, wd *plugintest.Work return fmt.Errorf("Error setting config: %w", err) } - // ----- Validate and configure the state store + // ----- Validate and configure the state store by running init err = runProviderCommand(ctx, t, wd, providers, func() error { return wd.Init(ctx) }) @@ -39,136 +36,100 @@ func testStepNewStateStore(ctx context.Context, t testing.T, wd *plugintest.Work return fmt.Errorf("Error running init: %w", err) } - // ----- Retrieve all the workspaces - workspaces := make([]string, 0) - err = runProviderCommand(ctx, t, wd, providers, func() error { - workspaces, err = wd.Workspaces(ctx) - if err != nil { - return err - } - - return nil - }) + // Assert the only workspace created after initialization is the "default" one + err = assertWorkspaces(ctx, t, wd, providers, []string{"default"}) if err != nil { - return fmt.Errorf("Error getting workspaces: %w", err) - } - - // Assert the only workspace created is the "default" one - // TODO:PSS: Might need to revisit this assertion for state stores. Not sure what the behavior will be during init, is this expected still? - // TODO:PSS: Is it possible to not support this for state stores or TF core backends? The cloud backend doesn't but that one is special :P - if len(workspaces) != 1 || workspaces[0] != "default" { - t.Fatalf("Expected a single workspace named \"default\" after initialization, got: %#v", workspaces) + return fmt.Errorf("After init, expected the \"default\" workspace to be created: %w", err) } // ----- Create "foo" workspace and assert the state returned is empty - err = runProviderCommand(ctx, t, wd, providers, func() error { - return wd.CreateWorkspace(ctx, "foo") - }) + err = createAndAssertEmptyWorkspace(ctx, t, wd, providers, "foo") if err != nil { - return fmt.Errorf("Error creating \"foo\" workspace: %w", err) + return fmt.Errorf("After creating a new workspace, the state should be empty: %w", err) } - var fooState *tfjson.State - err = runProviderCommand(ctx, t, wd, providers, func() error { - fooState, err = wd.State(ctx) - return err - }) + // ----- Create "bar" workspace and assert the state returned is empty + err = createAndAssertEmptyWorkspace(ctx, t, wd, providers, "bar") if err != nil { - return fmt.Errorf("Error retrieving \"foo\" state: %w", err) - } - - if fooState.Values != nil && fooState.Values.RootModule != nil && len(fooState.Values.RootModule.Resources) > 0 { - t.Fatalf("Expected the newly created \"foo\" state to be empty. Found %d resources.", len(fooState.Values.RootModule.Resources)) + return fmt.Errorf("After creating a new workspace, the state should be empty: %w", err) } - // ----- Create "bar" workspace and assert the state returned is empty - err = runProviderCommand(ctx, t, wd, providers, func() error { - return wd.CreateWorkspace(ctx, "bar") - }) + // ----- Apply test resources to the bar workspace and assert they are created successfully + err = applyTestResources(ctx, t, wd, step, providers, cfg, "bar") if err != nil { - return fmt.Errorf("Error creating \"bar\" workspace: %w", err) + return err } - var barState *tfjson.State - err = runProviderCommand(ctx, t, wd, providers, func() error { - barState, err = wd.State(ctx) - return err - }) + // ----- Switch to the "foo" workspace and grab the state, ensuring it's still empty. + err = assertEmptyWorkspace(ctx, t, wd, providers, "foo") if err != nil { - return fmt.Errorf("Error retrieving \"bar\" state: %w", err) + return fmt.Errorf("After writing a resource to \"bar\" state, failed assertion: %s", err) } - if barState.Values != nil && barState.Values.RootModule != nil && len(barState.Values.RootModule.Resources) > 0 { - t.Fatalf("Expected the newly created \"bar\" state to be empty. Found %d resources.", len(barState.Values.RootModule.Resources)) + // ----- Verify workspaces we get back are "default", "foo" (created during this test), and "bar" (created during this test). + err = assertWorkspaces(ctx, t, wd, providers, []string{"bar", "default", "foo"}) + if err != nil { + return err } - // ----- Add a fake resource to the bar workspace - barConfig := ` -resource "terraform_data" "tf_plugin_testing_resource_bar" { - input = "this resource was injected by terraform-plugin-testing" -}` - - // TODO:PSS: I'm 99% sure this should work with all of config file/directory implementations - cfgWithBar := cfg.Append(barConfig) - err = wd.SetConfig(ctx, cfgWithBar, step.ConfigVariables) + // ----- Delete "bar" workspace + err = deleteWorkspace(ctx, t, wd, providers, "bar") if err != nil { - return fmt.Errorf("Error setting config: %w", err) + return err } - // ----- Apply bar workspace + // ----- Attempt to delete "default" workspace, assert error err = runProviderCommand(ctx, t, wd, providers, func() error { - return wd.Apply(ctx) + return wd.SelectWorkspace(ctx, "foo") }) if err != nil { - return fmt.Errorf("Error creating fake resource in \"bar\" workspace: %w", err) + return fmt.Errorf("Error selecting \"foo\" workspace: %w", err) } - - // ----- Grab the bar state again err = runProviderCommand(ctx, t, wd, providers, func() error { - barState, err = wd.State(ctx) - return err + return wd.DeleteWorkspace(ctx, "default", tfexec.Force(true)) }) - if err != nil { - return fmt.Errorf("Error retrieving \"bar\" state: %w", err) + if err == nil { + return errors.New("Expected error when deleting \"default\" workspace") } - // ----- Check if the resource exists in the "bar" state - // TODO:PSS: Should we not use a statecheck here? Could just read the state manually... - checkOutput := statecheck.ExpectKnownValue( - "terraform_data.tf_plugin_testing_resource_bar", - tfjsonpath.New("output"), - knownvalue.StringExact("this resource was injected by terraform-plugin-testing"), - ) - - checkResp := statecheck.CheckStateResponse{} - checkOutput.CheckState(ctx, statecheck.CheckStateRequest{State: barState}, &checkResp) - - if checkResp.Error != nil { - return fmt.Errorf("After writing a test resource instance object to \"bar\" and re-reading it, the object has vanished: %w", err) + // ----- Recreate the "bar" workspace and assert it is empty (i.e. no left over artifacts) + err = createAndAssertEmptyWorkspace(ctx, t, wd, providers, "bar") + if err != nil { + return fmt.Errorf("After deleting, then recreating a new workspace, the state should be empty: %w", err) } - // ----- Switch to the "foo" workspace and grab the state, ensuring it's still empty. - err = runProviderCommand(ctx, t, wd, providers, func() error { - return wd.SelectWorkspace(ctx, "foo") - }) + // ----- Delete "bar" workspace again, force=true + err = deleteWorkspace(ctx, t, wd, providers, "bar") if err != nil { - return fmt.Errorf("Error creating \"foo\" workspace: %w", err) + return err } - err = runProviderCommand(ctx, t, wd, providers, func() error { - fooState, err = wd.State(ctx) + // ----- List workspaces and verify it's just "foo" and "default" + err = assertWorkspaces(ctx, t, wd, providers, []string{"default", "foo"}) + if err != nil { return err - }) + } + + // ----- Delete "foo" workspace, force=true + err = deleteWorkspace(ctx, t, wd, providers, "foo") if err != nil { - return fmt.Errorf("Error retrieving \"foo\" state: %w", err) + return err } - if fooState.Values != nil && fooState.Values.RootModule != nil && len(fooState.Values.RootModule.Resources) > 0 { - t.Fatalf("After writing a resource to \"bar\" state, expected the \"foo\" state to be empty. Found %d resources in \"foo\".", len(fooState.Values.RootModule.Resources)) + // ----- List workspaces and verify it's just "default" (which we did not modify) + err = assertWorkspaces(ctx, t, wd, providers, []string{"default"}) + if err != nil { + return err } - // ----- Verify when we list the workspaces we get back "default", "foo" (created during this test), and "bar" (created during this test). - workspaces = make([]string, 0) + return nil +} + +func assertWorkspaces(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, providers *providerFactories, expected []string) error { + t.Helper() + + var err error + workspaces := make([]string, 0) err = runProviderCommand(ctx, t, wd, providers, func() error { workspaces, err = wd.Workspaces(ctx) if err != nil { @@ -182,126 +143,147 @@ resource "terraform_data" "tf_plugin_testing_resource_bar" { } slices.Sort(workspaces) - expected := []string{"bar", "default", "foo"} if !slices.Equal(expected, workspaces) { - t.Fatalf("Expected workspaces to be %#v, got: %#v", expected, workspaces) + return fmt.Errorf("Expected workspaces to be %#v, got: %#v", expected, workspaces) } - // ----- Delete "bar" workspace + return nil +} - // Switch to "foo" workspace so we can delete bar. - err = runProviderCommand(ctx, t, wd, providers, func() error { - return wd.SelectWorkspace(ctx, "foo") - }) - if err != nil { - return fmt.Errorf("Error selecting \"foo\" workspace: %w", err) - } - err = runProviderCommand(ctx, t, wd, providers, func() error { - return wd.DeleteWorkspace(ctx, "bar", tfexec.Force(true)) +func createAndAssertEmptyWorkspace(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, providers *providerFactories, workspace string) error { + t.Helper() + + err := runProviderCommand(ctx, t, wd, providers, func() error { + return wd.CreateWorkspace(ctx, workspace) }) if err != nil { - return fmt.Errorf("Error deleting \"bar\" workspace: %w", err) + return fmt.Errorf("Error creating %q workspace: %w", workspace, err) } - // ----- Attempt to delete "default" workspace, assert error - err = runProviderCommand(ctx, t, wd, providers, func() error { - return wd.DeleteWorkspace(ctx, "default", tfexec.Force(true)) - }) - if err == nil { - return errors.New("Expected error when deleting \"default\" workspace") - } + return assertEmptyWorkspace(ctx, t, wd, providers, workspace) +} - // ----- Recreate the "bar" workspace - err = runProviderCommand(ctx, t, wd, providers, func() error { - return wd.CreateWorkspace(ctx, "bar") +func assertEmptyWorkspace(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, providers *providerFactories, workspace string) error { + t.Helper() + + err := runProviderCommand(ctx, t, wd, providers, func() error { + return wd.SelectWorkspace(ctx, workspace) }) if err != nil { - return fmt.Errorf("Error creating \"bar\" workspace: %w", err) + return fmt.Errorf("Error selecting %q workspace: %w", workspace, err) } - // ----- Grab "bar" state and assert it is empty (i.e. no left over artifacts) + var stateObj *tfjson.State err = runProviderCommand(ctx, t, wd, providers, func() error { - barState, err = wd.State(ctx) + stateObj, err = wd.State(ctx) return err }) if err != nil { - return fmt.Errorf("Error retrieving \"bar\" state: %w", err) + return fmt.Errorf("Error retrieving %q state: %w", workspace, err) } - if barState.Values != nil && barState.Values.RootModule != nil && len(barState.Values.RootModule.Resources) > 0 { - t.Fatalf("Expected the newly created \"bar\" state to be empty. Found %d resources.", len(barState.Values.RootModule.Resources)) + if stateObj.Values != nil && stateObj.Values.RootModule != nil && len(stateObj.Values.RootModule.Resources) > 0 { + return fmt.Errorf("Expected %q state to be empty. Found %d resources.", workspace, len(stateObj.Values.RootModule.Resources)) } - // ----- Delete "bar" workspace again, force=true + return nil +} - // Switch to "foo" workspace so we can delete bar. - err = runProviderCommand(ctx, t, wd, providers, func() error { - return wd.SelectWorkspace(ctx, "foo") +func deleteWorkspace(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, providers *providerFactories, workspace string) error { + t.Helper() + + // Select "default" workspace so we can delete the requested workspace + err := runProviderCommand(ctx, t, wd, providers, func() error { + return wd.SelectWorkspace(ctx, "default") }) if err != nil { - return fmt.Errorf("Error selecting \"foo\" workspace: %w", err) + return fmt.Errorf("Error selecting \"default\" workspace: %w", err) } err = runProviderCommand(ctx, t, wd, providers, func() error { - return wd.DeleteWorkspace(ctx, "bar", tfexec.Force(true)) + return wd.DeleteWorkspace(ctx, workspace, tfexec.Force(true)) }) if err != nil { - return fmt.Errorf("Error deleting \"bar\" workspace: %w", err) + return fmt.Errorf("Error deleting %q workspace: %w", workspace, err) } - // ----- List workspaces and verify it's just "foo" and "default" - workspaces = make([]string, 0) - err = runProviderCommand(ctx, t, wd, providers, func() error { - workspaces, err = wd.Workspaces(ctx) - if err != nil { - return err - } + return nil +} - return nil +// This is the primary place that state storage is being tested, we create two test resources (using the built-in terraform_data resource) with +// two different "terraform apply" commands, then check the state for their presence. +func applyTestResources(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, step TestStep, providers *providerFactories, cfg teststep.Config, workspace string) error { + t.Helper() + + err := runProviderCommand(ctx, t, wd, providers, func() error { + return wd.SelectWorkspace(ctx, workspace) }) if err != nil { - return fmt.Errorf("Error getting workspaces: %w", err) + return fmt.Errorf("Error selecting %q workspace: %w", workspace, err) } - slices.Sort(workspaces) - expected = []string{"default", "foo"} - if !slices.Equal(expected, workspaces) { - t.Fatalf("Expected workspaces to be %#v, got: %#v", expected, workspaces) - } + // ----- Apply test resource 1 to workspace + expectedOutput := "this resource was injected by terraform-plugin-testing" + testResourceCfg1 := fmt.Sprintf(` + resource "terraform_data" "tf_plugin_testing_resource_1" { + input = %q + }`, expectedOutput) - // ----- Delete "foo" workspace, force=true + cfg = cfg.Append(testResourceCfg1) + err = wd.SetConfig(ctx, cfg, step.ConfigVariables) + if err != nil { + return fmt.Errorf("Error setting config: %w", err) + } - // Switch to "default" workspace so we can delete bar. err = runProviderCommand(ctx, t, wd, providers, func() error { - return wd.SelectWorkspace(ctx, "default") + return wd.Apply(ctx) }) if err != nil { - return fmt.Errorf("Error selecting \"default\" workspace: %w", err) + return fmt.Errorf("Error creating test resource in %q workspace: %w", workspace, err) } + + // ----- Apply test resource 2 to workspace + testResourceCfg2 := fmt.Sprintf(` + resource "terraform_data" "tf_plugin_testing_resource_2" { + input = %q + }`, expectedOutput) + + cfg = cfg.Append(testResourceCfg2) + err = wd.SetConfig(ctx, cfg, step.ConfigVariables) + if err != nil { + return fmt.Errorf("Error setting config: %w", err) + } + err = runProviderCommand(ctx, t, wd, providers, func() error { - return wd.DeleteWorkspace(ctx, "foo", tfexec.Force(true)) + return wd.Apply(ctx) }) if err != nil { - return fmt.Errorf("Error deleting \"foo\" workspace: %w", err) + return fmt.Errorf("Error creating test resource in %q workspace: %w", workspace, err) } - // ----- List workspaces and verify it's just "default" (which we did not modify) - workspaces = make([]string, 0) + var stateObj *tfjson.State err = runProviderCommand(ctx, t, wd, providers, func() error { - workspaces, err = wd.Workspaces(ctx) - if err != nil { - return err - } - - return nil + stateObj, err = wd.State(ctx) + return err }) if err != nil { - return fmt.Errorf("Error getting workspaces: %w", err) + return fmt.Errorf("Error retrieving %q state: %w", workspace, err) } - slices.Sort(workspaces) - expected = []string{"default"} - if !slices.Equal(expected, workspaces) { - t.Fatalf("Expected workspaces to be %#v, got: %#v", expected, workspaces) + // ----- Check if the resources exist in the state + checkOutput := statecheck.ExpectKnownValue("terraform_data.tf_plugin_testing_resource_1", tfjsonpath.New("output"), knownvalue.StringExact(expectedOutput)) + checkResp := statecheck.CheckStateResponse{} + + checkOutput.CheckState(ctx, statecheck.CheckStateRequest{State: stateObj}, &checkResp) + if checkResp.Error != nil { + return fmt.Errorf("After writing a test resource instance object to %q state and re-reading it, the object has vanished: %w", workspace, checkResp.Error) + } + + checkOutput = statecheck.ExpectKnownValue("terraform_data.tf_plugin_testing_resource_2", tfjsonpath.New("output"), knownvalue.StringExact(expectedOutput)) + checkResp = statecheck.CheckStateResponse{} + + checkOutput.CheckState(ctx, statecheck.CheckStateRequest{State: stateObj}, &checkResp) + if checkResp.Error != nil { + return fmt.Errorf("After writing a test resource instance object to %q state and re-reading it, the object has vanished: %w", workspace, checkResp.Error) } return nil From 54d4f514070c032a260ead7d5f12ddb766866ca8 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 9 Jan 2026 16:09:12 -0500 Subject: [PATCH 05/19] refactoring --- .../testsdk/providerserver/providerserver.go | 62 ++++++------------- 1 file changed, 18 insertions(+), 44 deletions(-) diff --git a/internal/testing/testsdk/providerserver/providerserver.go b/internal/testing/testsdk/providerserver/providerserver.go index f36c1e56..a0fa72dc 100644 --- a/internal/testing/testsdk/providerserver/providerserver.go +++ b/internal/testing/testsdk/providerserver/providerserver.go @@ -1457,22 +1457,12 @@ func (s ProviderServer) ReadStateBytes(ctx context.Context, req *tfprotov6.ReadS totalLength := reader.Size() rangeStart := 0 - // TODO:PSS: Refactor -> Is there really no built-in to do this in Golang's standard library? resp.Chunks = func(yield func(tfprotov6.ReadStateByteChunk) bool) { for { - var diags []*tfprotov6.Diagnostic readBytes := make([]byte, chunkSize) byteCount, err := reader.Read(readBytes) if err != nil && !errors.Is(err, io.EOF) { - ok := yield(tfprotov6.ReadStateByteChunk{ - StateByteChunk: tfprotov6.StateByteChunk{ - Bytes: nil, - TotalLength: 0, - Range: tfprotov6.StateByteRange{ - Start: 0, - End: 0, - }, - }, + chunkWithDiag := tfprotov6.ReadStateByteChunk{ Diagnostics: []*tfprotov6.Diagnostic{ { Severity: tfprotov6.DiagnosticSeverityError, @@ -1483,39 +1473,31 @@ func (s ProviderServer) ReadStateBytes(ctx context.Context, req *tfprotov6.ReadS ), }, }, - }) - if !ok { + } + if !yield(chunkWithDiag) { return } } if byteCount == 0 { - // The previous iteration read the last byte of the data. - // - // We haven't used `yield` here, as yield doesn't return false.... for some reason (need to learn why). - // We need to return here to stop the iterator. - - // There is nothing to close here? - + // We've sent all of the bytes in the reader return } - ok := yield(tfprotov6.ReadStateByteChunk{ + chunk := tfprotov6.ReadStateByteChunk{ StateByteChunk: tfprotov6.StateByteChunk{ - Bytes: readBytes[0:byteCount], + Bytes: readBytes[:byteCount], TotalLength: int64(totalLength), Range: tfprotov6.StateByteRange{ Start: int64(rangeStart), End: int64(rangeStart + byteCount), }, }, - Diagnostics: diags, - }) - if !ok { + } + if !yield(chunk) { return } - // Track progress to ensure Range values are correct. rangeStart += byteCount } } @@ -1526,25 +1508,26 @@ func (s ProviderServer) ReadStateBytes(ctx context.Context, req *tfprotov6.ReadS func (s ProviderServer) WriteStateBytes(ctx context.Context, req *tfprotov6.WriteStateBytesStream) (*tfprotov6.WriteStateBytesResponse, error) { resp := &tfprotov6.WriteStateBytesResponse{} - // TODO:PSS: Refactor? - var buf bytes.Buffer - var chunkErr error + var stateBuffer bytes.Buffer var typeName string var stateId string for chunk := range req.Chunks { if chunk.Err != nil { - chunkErr = chunk.Err - break + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Error writing state", + Detail: fmt.Sprintf("An unexpected GRPC error occurred receieving state data from Terraform: %s", chunk.Err), + }) + return resp, nil } - // Can this be peeked using iter.Pull() ? if chunk.Meta != nil { typeName = chunk.Meta.TypeName stateId = chunk.Meta.StateId } - _, err := buf.Write(chunk.Bytes) + _, err := stateBuffer.Write(chunk.Bytes) if err != nil { resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ Severity: tfprotov6.DiagnosticSeverityError, @@ -1555,16 +1538,7 @@ func (s ProviderServer) WriteStateBytes(ctx context.Context, req *tfprotov6.Writ } } - if chunkErr != nil { - resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "Error writing state", - Detail: fmt.Sprintf("An unexpected GRPC error occurred receieving state data from Terraform: %s", chunkErr), - }) - return resp, nil - } - - if buf.Len() == 0 { + if stateBuffer.Len() == 0 { resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ Severity: tfprotov6.DiagnosticSeverityError, Summary: "Error writing state", @@ -1583,7 +1557,7 @@ func (s ProviderServer) WriteStateBytes(ctx context.Context, req *tfprotov6.Writ writeStateBytesReq := statestore.WriteStateBytesRequest{ StateID: stateId, - StateBytes: buf.Bytes(), + StateBytes: stateBuffer.Bytes(), } writeStateBytesResp := &statestore.WriteStateBytesResponse{} From 6bd1b27d72088a64c076d21a21ba8f717b03f60a Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 9 Jan 2026 16:11:30 -0500 Subject: [PATCH 06/19] remove comment --- helper/resource/statestore/statestore_test.go | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/helper/resource/statestore/statestore_test.go b/helper/resource/statestore/statestore_test.go index f038ba3a..3b6d436e 100644 --- a/helper/resource/statestore/statestore_test.go +++ b/helper/resource/statestore/statestore_test.go @@ -260,19 +260,3 @@ func TestStateStore_invalid_write_state(t *testing.T) { }, }) } - -// What we can test so far w/ smoke test: -// - State store validation -// - State store configuration -// - State store can write state -// - State store can read state -// - State store handles multiple workspaces - -// What is kind of awkward to test w/ smoke test -// - State size / chunking. You can do this by just changing the config you pass in w/ StateStore (we always inject a single resource to verify state apply will work) -// - Multiple applies to the same state (currently it just does one apply to the bar workspace) - -// What we can't test w/ smoke test -// - State store locks -// - State store unlocks -// - State store locks with a ton of clients (i.e. load test) From 766448bf22c18d4632dc5f7adf9a539de68b13d1 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 9 Jan 2026 16:43:22 -0500 Subject: [PATCH 07/19] fix up some comments --- helper/resource/statestore/examplecloud_statestore_test.go | 2 +- helper/resource/statestore/terraform_backend_test.go | 2 +- helper/resource/testing_new.go | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/helper/resource/statestore/examplecloud_statestore_test.go b/helper/resource/statestore/examplecloud_statestore_test.go index 962bea42..e3088e9a 100644 --- a/helper/resource/statestore/examplecloud_statestore_test.go +++ b/helper/resource/statestore/examplecloud_statestore_test.go @@ -71,7 +71,7 @@ func exampleCloudValidStateStore() *testprovider.StateStore { resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ Severity: tfprotov6.DiagnosticSeverityError, - Summary: fmt.Sprintf("Error state %q at path %q", req.StateID, stateFilePath), + Summary: fmt.Sprintf("Error reading state %q at path %q", req.StateID, stateFilePath), Detail: err.Error(), }) return diff --git a/helper/resource/statestore/terraform_backend_test.go b/helper/resource/statestore/terraform_backend_test.go index 3ba3c01d..9db3cb24 100644 --- a/helper/resource/statestore/terraform_backend_test.go +++ b/helper/resource/statestore/terraform_backend_test.go @@ -9,7 +9,7 @@ import ( // MAINTAINER NOTE: While the StateStore mode is designed to test state store implementations, it can // also be used to test existing Terraform core backends, which we do in this test file just for -// additional verification. +// additional verification of the test mode. func TestTerraformBackend_local(t *testing.T) { r.UnitTest(t, r.TestCase{ diff --git a/helper/resource/testing_new.go b/helper/resource/testing_new.go index 0322f714..a95297bc 100644 --- a/helper/resource/testing_new.go +++ b/helper/resource/testing_new.go @@ -412,7 +412,6 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest initializationErrorOccurred = true } - // Check if the error if step.ExpectError != nil { logging.HelperResourceDebug(ctx, "Checking TestStep ExpectError") if err == nil { From 4005b869cf24254ccb48ecbf3c2707605a06697a Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 9 Jan 2026 16:43:26 -0500 Subject: [PATCH 08/19] comments --- helper/resource/testing_new_state_store.go | 38 ++++++++++------------ 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/helper/resource/testing_new_state_store.go b/helper/resource/testing_new_state_store.go index c6500b19..8426bbf2 100644 --- a/helper/resource/testing_new_state_store.go +++ b/helper/resource/testing_new_state_store.go @@ -36,37 +36,37 @@ func testStepNewStateStore(ctx context.Context, t testing.T, wd *plugintest.Work return fmt.Errorf("Error running init: %w", err) } - // Assert the only workspace created after initialization is the "default" one + // After initialization the only workspace created should be "default" err = assertWorkspaces(ctx, t, wd, providers, []string{"default"}) if err != nil { return fmt.Errorf("After init, expected the \"default\" workspace to be created: %w", err) } - // ----- Create "foo" workspace and assert the state returned is empty + // ----- Create "foo" workspace err = createAndAssertEmptyWorkspace(ctx, t, wd, providers, "foo") if err != nil { return fmt.Errorf("After creating a new workspace, the state should be empty: %w", err) } - // ----- Create "bar" workspace and assert the state returned is empty + // ----- Create "bar" workspace err = createAndAssertEmptyWorkspace(ctx, t, wd, providers, "bar") if err != nil { return fmt.Errorf("After creating a new workspace, the state should be empty: %w", err) } - // ----- Apply test resources to the bar workspace and assert they are created successfully + // ----- Apply test resources to the "bar" workspace and assert they are saved successfully in state err = applyTestResources(ctx, t, wd, step, providers, cfg, "bar") if err != nil { return err } - // ----- Switch to the "foo" workspace and grab the state, ensuring it's still empty. + // ----- Assert the "foo" workspace is still empty err = assertEmptyWorkspace(ctx, t, wd, providers, "foo") if err != nil { return fmt.Errorf("After writing a resource to \"bar\" state, failed assertion: %s", err) } - // ----- Verify workspaces we get back are "default", "foo" (created during this test), and "bar" (created during this test). + // ----- Verify workspaces are "default", "foo" (created during this test), and "bar" (created during this test) err = assertWorkspaces(ctx, t, wd, providers, []string{"bar", "default", "foo"}) if err != nil { return err @@ -78,7 +78,7 @@ func testStepNewStateStore(ctx context.Context, t testing.T, wd *plugintest.Work return err } - // ----- Attempt to delete "default" workspace, assert error + // ----- Attempting to delete "default" workspace should return an error err = runProviderCommand(ctx, t, wd, providers, func() error { return wd.SelectWorkspace(ctx, "foo") }) @@ -98,7 +98,7 @@ func testStepNewStateStore(ctx context.Context, t testing.T, wd *plugintest.Work return fmt.Errorf("After deleting, then recreating a new workspace, the state should be empty: %w", err) } - // ----- Delete "bar" workspace again, force=true + // ----- Delete "bar" workspace err = deleteWorkspace(ctx, t, wd, providers, "bar") if err != nil { return err @@ -110,7 +110,7 @@ func testStepNewStateStore(ctx context.Context, t testing.T, wd *plugintest.Work return err } - // ----- Delete "foo" workspace, force=true + // ----- Delete "foo" workspace err = deleteWorkspace(ctx, t, wd, providers, "foo") if err != nil { return err @@ -129,22 +129,20 @@ func assertWorkspaces(ctx context.Context, t testing.T, wd *plugintest.WorkingDi t.Helper() var err error - workspaces := make([]string, 0) + actualWorkspaces := make([]string, 0) err = runProviderCommand(ctx, t, wd, providers, func() error { - workspaces, err = wd.Workspaces(ctx) - if err != nil { - return err - } - - return nil + actualWorkspaces, err = wd.Workspaces(ctx) + return err }) if err != nil { return fmt.Errorf("Error getting workspaces: %w", err) } - slices.Sort(workspaces) - if !slices.Equal(expected, workspaces) { - return fmt.Errorf("Expected workspaces to be %#v, got: %#v", expected, workspaces) + slices.Sort(expected) + slices.Sort(actualWorkspaces) + + if !slices.Equal(expected, actualWorkspaces) { + return fmt.Errorf("Expected workspaces to be %#v, got: %#v", expected, actualWorkspaces) } return nil @@ -260,6 +258,7 @@ func applyTestResources(ctx context.Context, t testing.T, wd *plugintest.Working return fmt.Errorf("Error creating test resource in %q workspace: %w", workspace, err) } + // ----- Check if the resources exist in the state var stateObj *tfjson.State err = runProviderCommand(ctx, t, wd, providers, func() error { stateObj, err = wd.State(ctx) @@ -269,7 +268,6 @@ func applyTestResources(ctx context.Context, t testing.T, wd *plugintest.Working return fmt.Errorf("Error retrieving %q state: %w", workspace, err) } - // ----- Check if the resources exist in the state checkOutput := statecheck.ExpectKnownValue("terraform_data.tf_plugin_testing_resource_1", tfjsonpath.New("output"), knownvalue.StringExact(expectedOutput)) checkResp := statecheck.CheckStateResponse{} From 826860a8394de0831200aaf35b5a3de35a22ed65 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 9 Jan 2026 17:10:59 -0500 Subject: [PATCH 09/19] changelog --- .changes/unreleased/FEATURES-20260109-170955.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changes/unreleased/FEATURES-20260109-170955.yaml diff --git a/.changes/unreleased/FEATURES-20260109-170955.yaml b/.changes/unreleased/FEATURES-20260109-170955.yaml new file mode 100644 index 00000000..32b771ae --- /dev/null +++ b/.changes/unreleased/FEATURES-20260109-170955.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'helper/resource: Added new `StateStore` testing mode to `TestStep`, which can be used to smoke test Terraform state storage.' +time: 2026-01-09T17:09:55.56054-05:00 +custom: + Issue: "591" From e745e5259a9285a47ba88bad126e555646e14ac6 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 9 Jan 2026 17:12:59 -0500 Subject: [PATCH 10/19] add copywrite headers (manually :P) --- helper/resource/statestore/examplecloud_statestore_test.go | 3 +++ helper/resource/statestore/statestore_test.go | 3 +++ helper/resource/statestore/terraform_backend_test.go | 3 +++ helper/resource/testing_new_state_store.go | 3 +++ internal/testing/testprovider/statestore.go | 2 +- internal/testing/testsdk/providerserver/statestores.go | 2 +- internal/testing/testsdk/statestore/statestore.go | 3 +++ 7 files changed, 17 insertions(+), 2 deletions(-) diff --git a/helper/resource/statestore/examplecloud_statestore_test.go b/helper/resource/statestore/examplecloud_statestore_test.go index e3088e9a..d2a6247d 100644 --- a/helper/resource/statestore/examplecloud_statestore_test.go +++ b/helper/resource/statestore/examplecloud_statestore_test.go @@ -1,3 +1,6 @@ +// Copyright IBM Corp. 2014, 2025 +// SPDX-License-Identifier: MPL-2.0 + package statestore_test import ( diff --git a/helper/resource/statestore/statestore_test.go b/helper/resource/statestore/statestore_test.go index 3b6d436e..0d6d72fd 100644 --- a/helper/resource/statestore/statestore_test.go +++ b/helper/resource/statestore/statestore_test.go @@ -1,3 +1,6 @@ +// Copyright IBM Corp. 2014, 2025 +// SPDX-License-Identifier: MPL-2.0 + package statestore_test import ( diff --git a/helper/resource/statestore/terraform_backend_test.go b/helper/resource/statestore/terraform_backend_test.go index 9db3cb24..61c3952e 100644 --- a/helper/resource/statestore/terraform_backend_test.go +++ b/helper/resource/statestore/terraform_backend_test.go @@ -1,3 +1,6 @@ +// Copyright IBM Corp. 2014, 2025 +// SPDX-License-Identifier: MPL-2.0 + package statestore_test import ( diff --git a/helper/resource/testing_new_state_store.go b/helper/resource/testing_new_state_store.go index 8426bbf2..1309056c 100644 --- a/helper/resource/testing_new_state_store.go +++ b/helper/resource/testing_new_state_store.go @@ -1,3 +1,6 @@ +// Copyright IBM Corp. 2014, 2025 +// SPDX-License-Identifier: MPL-2.0 + package resource import ( diff --git a/internal/testing/testprovider/statestore.go b/internal/testing/testprovider/statestore.go index a9e54118..192f7aab 100644 --- a/internal/testing/testprovider/statestore.go +++ b/internal/testing/testprovider/statestore.go @@ -1,4 +1,4 @@ -// Copyright (c) HashiCorp, Inc. +// Copyright IBM Corp. 2014, 2025 // SPDX-License-Identifier: MPL-2.0 package testprovider diff --git a/internal/testing/testsdk/providerserver/statestores.go b/internal/testing/testsdk/providerserver/statestores.go index b7d18932..5b16e8ee 100644 --- a/internal/testing/testsdk/providerserver/statestores.go +++ b/internal/testing/testsdk/providerserver/statestores.go @@ -1,4 +1,4 @@ -// Copyright (c) HashiCorp, Inc. +// Copyright IBM Corp. 2014, 2025 // SPDX-License-Identifier: MPL-2.0 package providerserver diff --git a/internal/testing/testsdk/statestore/statestore.go b/internal/testing/testsdk/statestore/statestore.go index f3388573..3065fca6 100644 --- a/internal/testing/testsdk/statestore/statestore.go +++ b/internal/testing/testsdk/statestore/statestore.go @@ -1,3 +1,6 @@ +// Copyright IBM Corp. 2014, 2025 +// SPDX-License-Identifier: MPL-2.0 + package statestore import ( From fcf151f5450f3a9b5a4280c0382704cd13422494 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 9 Jan 2026 17:16:11 -0500 Subject: [PATCH 11/19] linting errors fixed --- helper/resource/statestore/terraform_backend_test.go | 4 ++++ internal/testing/testsdk/providerserver/providerserver.go | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/helper/resource/statestore/terraform_backend_test.go b/helper/resource/statestore/terraform_backend_test.go index 61c3952e..2e304a89 100644 --- a/helper/resource/statestore/terraform_backend_test.go +++ b/helper/resource/statestore/terraform_backend_test.go @@ -15,6 +15,8 @@ import ( // additional verification of the test mode. func TestTerraformBackend_local(t *testing.T) { + t.Parallel() + r.UnitTest(t, r.TestCase{ // MAINTAINER NOTE: Test steps won't run without a provider definition, so this is just // needed to pass validation, as we're just testing Terraform core itself. @@ -35,6 +37,8 @@ func TestTerraformBackend_local(t *testing.T) { } func TestTerraformBackend_local_empty_path_validation_error(t *testing.T) { + t.Parallel() + r.UnitTest(t, r.TestCase{ // MAINTAINER NOTE: Test steps won't run without a provider definition, so this is just // needed to pass validation, as we're just testing Terraform core itself. diff --git a/internal/testing/testsdk/providerserver/providerserver.go b/internal/testing/testsdk/providerserver/providerserver.go index a0fa72dc..65cc33d5 100644 --- a/internal/testing/testsdk/providerserver/providerserver.go +++ b/internal/testing/testsdk/providerserver/providerserver.go @@ -1487,7 +1487,7 @@ func (s ProviderServer) ReadStateBytes(ctx context.Context, req *tfprotov6.ReadS chunk := tfprotov6.ReadStateByteChunk{ StateByteChunk: tfprotov6.StateByteChunk{ Bytes: readBytes[:byteCount], - TotalLength: int64(totalLength), + TotalLength: totalLength, Range: tfprotov6.StateByteRange{ Start: int64(rangeStart), End: int64(rangeStart + byteCount), From 37f703c229fbaa32e972b6872490a868863495ec Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 9 Jan 2026 17:22:08 -0500 Subject: [PATCH 12/19] add skips for pre 1.4 --- helper/resource/statestore/terraform_backend_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/helper/resource/statestore/terraform_backend_test.go b/helper/resource/statestore/terraform_backend_test.go index 2e304a89..5ba5cbf1 100644 --- a/helper/resource/statestore/terraform_backend_test.go +++ b/helper/resource/statestore/terraform_backend_test.go @@ -8,6 +8,7 @@ import ( "testing" r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" ) // MAINTAINER NOTE: While the StateStore mode is designed to test state store implementations, it can @@ -18,6 +19,10 @@ func TestTerraformBackend_local(t *testing.T) { t.Parallel() r.UnitTest(t, r.TestCase{ + // StateStore test mode uses `terraform_data` + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_0), + }, // MAINTAINER NOTE: Test steps won't run without a provider definition, so this is just // needed to pass validation, as we're just testing Terraform core itself. ExternalProviders: map[string]r.ExternalProvider{ @@ -40,6 +45,10 @@ func TestTerraformBackend_local_empty_path_validation_error(t *testing.T) { t.Parallel() r.UnitTest(t, r.TestCase{ + // StateStore test mode uses `terraform_data` + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_4_0), + }, // MAINTAINER NOTE: Test steps won't run without a provider definition, so this is just // needed to pass validation, as we're just testing Terraform core itself. ExternalProviders: map[string]r.ExternalProvider{ From 1772faf0a921840cda1048dc213aef7b2b0ddf3c Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 16 Jan 2026 17:16:23 -0500 Subject: [PATCH 13/19] use main for tf-exec --- go.mod | 4 ++-- go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 5f193958..fd403ef5 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/hashicorp/terraform-plugin-testing -go 1.24.10 +go 1.24.0 toolchain go1.24.11 @@ -13,7 +13,7 @@ require ( github.com/hashicorp/hc-install v0.9.2 github.com/hashicorp/hcl/v2 v2.24.0 github.com/hashicorp/logutils v1.0.0 - github.com/hashicorp/terraform-exec v0.24.1-0.20260106235132-34cefc28ca08 + github.com/hashicorp/terraform-exec v0.24.1-0.20260116164333-91663fc705d2 github.com/hashicorp/terraform-json v0.27.2 github.com/hashicorp/terraform-plugin-go v0.29.1-0.20251107103451-b1dbec9688da github.com/hashicorp/terraform-plugin-log v0.10.0 diff --git a/go.sum b/go.sum index 0640f5cb..0ae4ecc5 100644 --- a/go.sum +++ b/go.sum @@ -77,8 +77,8 @@ github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQx github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.24.1-0.20260106235132-34cefc28ca08 h1:Rz8MOZH3cAHvsgqDwMj65a1ImagpAZgzdOYn8uHsEFM= -github.com/hashicorp/terraform-exec v0.24.1-0.20260106235132-34cefc28ca08/go.mod h1:ZkcO1aTx9rwgeW5XEhdrxRKQtxhPvf6lVQvrtioo/no= +github.com/hashicorp/terraform-exec v0.24.1-0.20260116164333-91663fc705d2 h1:37Gp5F1n4gV1MrPhEOyKt487/X4nOxdNCaPIx8iT1Lw= +github.com/hashicorp/terraform-exec v0.24.1-0.20260116164333-91663fc705d2/go.mod h1:/+qarFaJUhdMK58b2hpaEEypGqsGPfauUuSOiengLr4= github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoKST/tRDBJKU= github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= github.com/hashicorp/terraform-plugin-go v0.29.1-0.20251107103451-b1dbec9688da h1:RvFdseryCMkoBWxucUSEnQkeROpBiungf3bNQV1efgc= From ae9b9094b62d92e77f5e084c306e06f2d7363058 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 16 Jan 2026 17:17:00 -0500 Subject: [PATCH 14/19] remove toolchain --- go.mod | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.mod b/go.mod index fd403ef5..fcdddd42 100644 --- a/go.mod +++ b/go.mod @@ -2,8 +2,6 @@ module github.com/hashicorp/terraform-plugin-testing go 1.24.0 -toolchain go1.24.11 - require ( github.com/google/go-cmp v0.7.0 github.com/hashicorp/go-cty v1.5.0 From 11bc335e2bf748eb2c57f1ed170f9edba5dc8778 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 16 Jan 2026 17:20:04 -0500 Subject: [PATCH 15/19] tf-exec wrong dep --- go.mod | 8 ++++---- go.sum | 40 ++++++++++++++++++++-------------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index fcdddd42..2dc16aa6 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/hashicorp/hc-install v0.9.2 github.com/hashicorp/hcl/v2 v2.24.0 github.com/hashicorp/logutils v1.0.0 - github.com/hashicorp/terraform-exec v0.24.1-0.20260116164333-91663fc705d2 + github.com/hashicorp/terraform-exec v0.24.1-0.20260116221908-66fa38689b13 github.com/hashicorp/terraform-json v0.27.2 github.com/hashicorp/terraform-plugin-go v0.29.1-0.20251107103451-b1dbec9688da github.com/hashicorp/terraform-plugin-log v0.10.0 @@ -54,7 +54,7 @@ require ( golang.org/x/text v0.33.0 // indirect golang.org/x/tools v0.40.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect - google.golang.org/grpc v1.76.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect + google.golang.org/grpc v1.78.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/go.sum b/go.sum index 0ae4ecc5..bea2fdc6 100644 --- a/go.sum +++ b/go.sum @@ -77,8 +77,8 @@ github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQx github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.24.1-0.20260116164333-91663fc705d2 h1:37Gp5F1n4gV1MrPhEOyKt487/X4nOxdNCaPIx8iT1Lw= -github.com/hashicorp/terraform-exec v0.24.1-0.20260116164333-91663fc705d2/go.mod h1:/+qarFaJUhdMK58b2hpaEEypGqsGPfauUuSOiengLr4= +github.com/hashicorp/terraform-exec v0.24.1-0.20260116221908-66fa38689b13 h1:3RuWE+KU9G9Yg8dsS3TEDRXTq4CCjlCStIBuaFVURM4= +github.com/hashicorp/terraform-exec v0.24.1-0.20260116221908-66fa38689b13/go.mod h1:/+qarFaJUhdMK58b2hpaEEypGqsGPfauUuSOiengLr4= github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoKST/tRDBJKU= github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= github.com/hashicorp/terraform-plugin-go v0.29.1-0.20251107103451-b1dbec9688da h1:RvFdseryCMkoBWxucUSEnQkeROpBiungf3bNQV1efgc= @@ -155,18 +155,18 @@ github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0 github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= @@ -221,14 +221,14 @@ gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= From fb23c4fb83f301534d978b879415ae7eb93c54eb Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 16 Jan 2026 17:38:20 -0500 Subject: [PATCH 16/19] use new plugin-go changes --- go.mod | 2 +- go.sum | 4 +-- internal/testing/testprovider/statestore.go | 5 ++- .../testsdk/providerserver/providerserver.go | 34 ++++++++----------- .../testing/testsdk/statestore/statestore.go | 4 +-- 5 files changed, 24 insertions(+), 25 deletions(-) diff --git a/go.mod b/go.mod index 2dc16aa6..e5d878d9 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/hashicorp/logutils v1.0.0 github.com/hashicorp/terraform-exec v0.24.1-0.20260116221908-66fa38689b13 github.com/hashicorp/terraform-json v0.27.2 - github.com/hashicorp/terraform-plugin-go v0.29.1-0.20251107103451-b1dbec9688da + github.com/hashicorp/terraform-plugin-go v0.29.1-0.20260116223458-7014ea6dbfa8 github.com/hashicorp/terraform-plugin-log v0.10.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1 github.com/mitchellh/go-testing-interface v1.14.1 diff --git a/go.sum b/go.sum index bea2fdc6..e0cd5168 100644 --- a/go.sum +++ b/go.sum @@ -81,8 +81,8 @@ github.com/hashicorp/terraform-exec v0.24.1-0.20260116221908-66fa38689b13 h1:3Ru github.com/hashicorp/terraform-exec v0.24.1-0.20260116221908-66fa38689b13/go.mod h1:/+qarFaJUhdMK58b2hpaEEypGqsGPfauUuSOiengLr4= github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoKST/tRDBJKU= github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= -github.com/hashicorp/terraform-plugin-go v0.29.1-0.20251107103451-b1dbec9688da h1:RvFdseryCMkoBWxucUSEnQkeROpBiungf3bNQV1efgc= -github.com/hashicorp/terraform-plugin-go v0.29.1-0.20251107103451-b1dbec9688da/go.mod h1:KHRnT9vExG+r1fLxwOzOP6C6YXiaKHtsCIrky7teDYc= +github.com/hashicorp/terraform-plugin-go v0.29.1-0.20260116223458-7014ea6dbfa8 h1:wDyLNpSOc94dg0qIGShq0haNJIvzyir/RQefgLZNpC0= +github.com/hashicorp/terraform-plugin-go v0.29.1-0.20260116223458-7014ea6dbfa8/go.mod h1:q0rLI/Z8QaAhX+22MvIlGbFkHoOtwS58vaShJZPXndU= github.com/hashicorp/terraform-plugin-log v0.10.0 h1:eu2kW6/QBVdN4P3Ju2WiB2W3ObjkAsyfBsL3Wh1fj3g= github.com/hashicorp/terraform-plugin-log v0.10.0/go.mod h1:/9RR5Cv2aAbrqcTSdNmY1NRHP4E3ekrXRGjqORpXyB0= github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1 h1:mlAq/OrMlg04IuJT7NpefI1wwtdpWudnEmjuQs04t/4= diff --git a/internal/testing/testprovider/statestore.go b/internal/testing/testprovider/statestore.go index 192f7aab..b77348f1 100644 --- a/internal/testing/testprovider/statestore.go +++ b/internal/testing/testprovider/statestore.go @@ -38,7 +38,10 @@ func (s *StateStore) Schema(ctx context.Context, req statestore.SchemaRequest, r func (s *StateStore) Configure(ctx context.Context, req statestore.ConfigureRequest, resp *statestore.ConfigureResponse) { if s.ConfigureResponse != nil { resp.Diagnostics = s.ConfigureResponse.Diagnostics - resp.ServerCapabilities = s.ConfigureResponse.ServerCapabilities + + if s.ConfigureResponse.ServerCapabilities != nil { + resp.ServerCapabilities = s.ConfigureResponse.ServerCapabilities + } } // Store configured chunk size diff --git a/internal/testing/testsdk/providerserver/providerserver.go b/internal/testing/testsdk/providerserver/providerserver.go index 65cc33d5..1334024c 100644 --- a/internal/testing/testsdk/providerserver/providerserver.go +++ b/internal/testing/testsdk/providerserver/providerserver.go @@ -1281,7 +1281,7 @@ func (s ProviderServer) ConfigureStateStore(ctx context.Context, req *tfprotov6. } configureResp := &statestore.ConfigureResponse{ // Round-trip the core-provided chunk size as the default - ServerCapabilities: tfprotov6.StateStoreServerCapabilities{ + ServerCapabilities: &tfprotov6.StateStoreServerCapabilities{ ChunkSize: req.Capabilities.ChunkSize, }, } @@ -1294,8 +1294,8 @@ func (s ProviderServer) ConfigureStateStore(ctx context.Context, req *tfprotov6. return resp, nil } -func (s ProviderServer) ValidateStateStoreConfig(ctx context.Context, req *tfprotov6.ValidateStateStoreRequest) (*tfprotov6.ValidateStateStoreResponse, error) { - resp := &tfprotov6.ValidateStateStoreResponse{} +func (s ProviderServer) ValidateStateStoreConfig(ctx context.Context, req *tfprotov6.ValidateStateStoreConfigRequest) (*tfprotov6.ValidateStateStoreConfigResponse, error) { + resp := &tfprotov6.ValidateStateStoreConfigResponse{} store, diag := ProviderStateStore(s.Provider, req.TypeName) @@ -1353,7 +1353,7 @@ func (s ProviderServer) GetStates(ctx context.Context, req *tfprotov6.GetStatesR store.GetStates(ctx, getStatesReq, getStatesResp) resp.Diagnostics = getStatesResp.Diagnostics - resp.StateId = getStatesResp.StateIDs + resp.StateIDs = getStatesResp.StateIDs return resp, nil } @@ -1370,7 +1370,7 @@ func (s ProviderServer) DeleteState(ctx context.Context, req *tfprotov6.DeleteSt } deleteStateReq := statestore.DeleteStateRequest{ - StateID: req.StateId, + StateID: req.StateID, } deleteStateResp := &statestore.DeleteStateResponse{} @@ -1393,7 +1393,7 @@ func (s ProviderServer) LockState(ctx context.Context, req *tfprotov6.LockStateR } lockStateReq := statestore.LockStateRequest{ - StateID: req.StateId, + StateID: req.StateID, Operation: req.Operation, } lockStateResp := &statestore.LockStateResponse{} @@ -1401,7 +1401,7 @@ func (s ProviderServer) LockState(ctx context.Context, req *tfprotov6.LockStateR store.LockState(ctx, lockStateReq, lockStateResp) resp.Diagnostics = lockStateResp.Diagnostics - resp.LockId = lockStateResp.LockID + resp.LockID = lockStateResp.LockID return resp, nil } @@ -1418,8 +1418,8 @@ func (s ProviderServer) UnlockState(ctx context.Context, req *tfprotov6.UnlockSt } lockStateReq := statestore.UnlockStateRequest{ - StateID: req.StateId, - LockID: req.LockId, + StateID: req.StateID, + LockID: req.LockID, } lockStateResp := &statestore.UnlockStateResponse{} @@ -1441,7 +1441,7 @@ func (s ProviderServer) ReadStateBytes(ctx context.Context, req *tfprotov6.ReadS } readStateBytesReq := statestore.ReadStateBytesRequest{ - StateID: req.StateId, + StateID: req.StateID, } readStateBytesResp := &statestore.ReadStateBytesResponse{} @@ -1468,7 +1468,7 @@ func (s ProviderServer) ReadStateBytes(ctx context.Context, req *tfprotov6.ReadS Severity: tfprotov6.DiagnosticSeverityError, Summary: "Error reading state", Detail: fmt.Sprintf("An unexpected error occurred while reading state data for %s: %s", - req.StateId, + req.StateID, err, ), }, @@ -1512,19 +1512,15 @@ func (s ProviderServer) WriteStateBytes(ctx context.Context, req *tfprotov6.Writ var typeName string var stateId string - for chunk := range req.Chunks { - if chunk.Err != nil { - resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "Error writing state", - Detail: fmt.Sprintf("An unexpected GRPC error occurred receieving state data from Terraform: %s", chunk.Err), - }) + for chunk, diags := range req.Chunks { + if len(diags) > 0 { + resp.Diagnostics = append(resp.Diagnostics, diags...) return resp, nil } if chunk.Meta != nil { typeName = chunk.Meta.TypeName - stateId = chunk.Meta.StateId + stateId = chunk.Meta.StateID } _, err := stateBuffer.Write(chunk.Bytes) diff --git a/internal/testing/testsdk/statestore/statestore.go b/internal/testing/testsdk/statestore/statestore.go index 3065fca6..78a58300 100644 --- a/internal/testing/testsdk/statestore/statestore.go +++ b/internal/testing/testsdk/statestore/statestore.go @@ -36,12 +36,12 @@ type SchemaResponse struct { type ConfigureRequest struct { Config tftypes.Value - ClientCapabilities tfprotov6.StateStoreClientCapabilities + ClientCapabilities *tfprotov6.ConfigureStateStoreClientCapabilities } type ConfigureResponse struct { Diagnostics []*tfprotov6.Diagnostic - ServerCapabilities tfprotov6.StateStoreServerCapabilities + ServerCapabilities *tfprotov6.StateStoreServerCapabilities } type ValidateConfigRequest struct { From e688b1cd28cef5ea5a0d64b06c29df8c1cfe708d Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 20 Jan 2026 17:05:58 -0500 Subject: [PATCH 17/19] update plugin go --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e5d878d9..6bb59131 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/hashicorp/logutils v1.0.0 github.com/hashicorp/terraform-exec v0.24.1-0.20260116221908-66fa38689b13 github.com/hashicorp/terraform-json v0.27.2 - github.com/hashicorp/terraform-plugin-go v0.29.1-0.20260116223458-7014ea6dbfa8 + github.com/hashicorp/terraform-plugin-go v0.29.1-0.20260120220013-b2f99cb946ea github.com/hashicorp/terraform-plugin-log v0.10.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1 github.com/mitchellh/go-testing-interface v1.14.1 diff --git a/go.sum b/go.sum index e0cd5168..b19cf055 100644 --- a/go.sum +++ b/go.sum @@ -81,8 +81,8 @@ github.com/hashicorp/terraform-exec v0.24.1-0.20260116221908-66fa38689b13 h1:3Ru github.com/hashicorp/terraform-exec v0.24.1-0.20260116221908-66fa38689b13/go.mod h1:/+qarFaJUhdMK58b2hpaEEypGqsGPfauUuSOiengLr4= github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoKST/tRDBJKU= github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= -github.com/hashicorp/terraform-plugin-go v0.29.1-0.20260116223458-7014ea6dbfa8 h1:wDyLNpSOc94dg0qIGShq0haNJIvzyir/RQefgLZNpC0= -github.com/hashicorp/terraform-plugin-go v0.29.1-0.20260116223458-7014ea6dbfa8/go.mod h1:q0rLI/Z8QaAhX+22MvIlGbFkHoOtwS58vaShJZPXndU= +github.com/hashicorp/terraform-plugin-go v0.29.1-0.20260120220013-b2f99cb946ea h1:nGrHefpx/i05z/sF9NwxDgCszMFCUUmjszuBGP7KWgc= +github.com/hashicorp/terraform-plugin-go v0.29.1-0.20260120220013-b2f99cb946ea/go.mod h1:q0rLI/Z8QaAhX+22MvIlGbFkHoOtwS58vaShJZPXndU= github.com/hashicorp/terraform-plugin-log v0.10.0 h1:eu2kW6/QBVdN4P3Ju2WiB2W3ObjkAsyfBsL3Wh1fj3g= github.com/hashicorp/terraform-plugin-log v0.10.0/go.mod h1:/9RR5Cv2aAbrqcTSdNmY1NRHP4E3ekrXRGjqORpXyB0= github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1 h1:mlAq/OrMlg04IuJT7NpefI1wwtdpWudnEmjuQs04t/4= From b42f2caf05a74d1835204ac25799ffdcf1eb2706 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 12:58:22 -0500 Subject: [PATCH 18/19] build(deps): Bump actions/setup-go in the github-actions group (#594) Bumps the github-actions group with 1 update: [actions/setup-go](https://github.com/actions/setup-go). Updates `actions/setup-go` from 6.1.0 to 6.2.0 - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/4dc6199c7b1a012772edbd06daecab0f50c9053c...7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5) --- updated-dependencies: - dependency-name: actions/setup-go dependency-version: 6.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-github-actions.yml | 2 +- .github/workflows/ci-go.yml | 4 ++-- .github/workflows/ci-goreleaser.yml | 2 +- .github/workflows/release.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-github-actions.yml b/.github/workflows/ci-github-actions.yml index 6ecfc6c8..129e929c 100644 --- a/.github/workflows/ci-github-actions.yml +++ b/.github/workflows/ci-github-actions.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version-file: 'go.mod' - run: go install github.com/rhysd/actionlint/cmd/actionlint@latest diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index acb3bed8..fe33b843 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version-file: 'go.mod' - run: go mod download @@ -31,7 +31,7 @@ jobs: terraform: ${{ fromJSON(vars.TF_VERSIONS_PROTOCOL_V5) }} steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version: ${{ matrix.go-version }} - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 diff --git a/.github/workflows/ci-goreleaser.yml b/.github/workflows/ci-goreleaser.yml index c5786a4a..411b71fc 100644 --- a/.github/workflows/ci-goreleaser.yml +++ b/.github/workflows/ci-goreleaser.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version-file: 'go.mod' - uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bb77e409..93b31ef5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,7 +84,7 @@ jobs: ref: ${{ inputs.versionNumber }} fetch-depth: 0 - - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version-file: 'go.mod' From 8dbeb5f8b652434d80a9759d89d523196f70e4ce Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 22 Jan 2026 15:47:38 -0500 Subject: [PATCH 19/19] update to use main branch commits --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 6bb59131..1bdcf43b 100644 --- a/go.mod +++ b/go.mod @@ -11,9 +11,9 @@ require ( github.com/hashicorp/hc-install v0.9.2 github.com/hashicorp/hcl/v2 v2.24.0 github.com/hashicorp/logutils v1.0.0 - github.com/hashicorp/terraform-exec v0.24.1-0.20260116221908-66fa38689b13 + github.com/hashicorp/terraform-exec v0.24.1-0.20260121172827-e91e9600ec9b github.com/hashicorp/terraform-json v0.27.2 - github.com/hashicorp/terraform-plugin-go v0.29.1-0.20260120220013-b2f99cb946ea + github.com/hashicorp/terraform-plugin-go v0.29.1-0.20260122204301-b0a6aca80fd7 github.com/hashicorp/terraform-plugin-log v0.10.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1 github.com/mitchellh/go-testing-interface v1.14.1 diff --git a/go.sum b/go.sum index b19cf055..c53a2a6a 100644 --- a/go.sum +++ b/go.sum @@ -77,12 +77,12 @@ github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQx github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.24.1-0.20260116221908-66fa38689b13 h1:3RuWE+KU9G9Yg8dsS3TEDRXTq4CCjlCStIBuaFVURM4= -github.com/hashicorp/terraform-exec v0.24.1-0.20260116221908-66fa38689b13/go.mod h1:/+qarFaJUhdMK58b2hpaEEypGqsGPfauUuSOiengLr4= +github.com/hashicorp/terraform-exec v0.24.1-0.20260121172827-e91e9600ec9b h1:fJUuEi2V7XraZP/KFvfEsfG+YoszjVp1jz1wRH2g3c0= +github.com/hashicorp/terraform-exec v0.24.1-0.20260121172827-e91e9600ec9b/go.mod h1:/+qarFaJUhdMK58b2hpaEEypGqsGPfauUuSOiengLr4= github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoKST/tRDBJKU= github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= -github.com/hashicorp/terraform-plugin-go v0.29.1-0.20260120220013-b2f99cb946ea h1:nGrHefpx/i05z/sF9NwxDgCszMFCUUmjszuBGP7KWgc= -github.com/hashicorp/terraform-plugin-go v0.29.1-0.20260120220013-b2f99cb946ea/go.mod h1:q0rLI/Z8QaAhX+22MvIlGbFkHoOtwS58vaShJZPXndU= +github.com/hashicorp/terraform-plugin-go v0.29.1-0.20260122204301-b0a6aca80fd7 h1:ViO8kjIwBFTqOrMT5GilsY8TKRjejAqSTyFjjdTMY1o= +github.com/hashicorp/terraform-plugin-go v0.29.1-0.20260122204301-b0a6aca80fd7/go.mod h1:q0rLI/Z8QaAhX+22MvIlGbFkHoOtwS58vaShJZPXndU= github.com/hashicorp/terraform-plugin-log v0.10.0 h1:eu2kW6/QBVdN4P3Ju2WiB2W3ObjkAsyfBsL3Wh1fj3g= github.com/hashicorp/terraform-plugin-log v0.10.0/go.mod h1:/9RR5Cv2aAbrqcTSdNmY1NRHP4E3ekrXRGjqORpXyB0= github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1 h1:mlAq/OrMlg04IuJT7NpefI1wwtdpWudnEmjuQs04t/4=