diff --git a/go.mod b/go.mod index c0b835569..bc32ed306 100644 --- a/go.mod +++ b/go.mod @@ -13,9 +13,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.23.0 + github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2 github.com/hashicorp/terraform-json v0.25.0 - github.com/hashicorp/terraform-plugin-go v0.28.0 + github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 github.com/mitchellh/go-testing-interface v1.14.1 @@ -36,7 +36,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-plugin v1.6.3 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect - github.com/hashicorp/terraform-registry-address v0.2.5 // indirect + github.com/hashicorp/terraform-registry-address v0.3.0 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -56,7 +56,7 @@ require ( golang.org/x/text v0.27.0 // indirect golang.org/x/tools v0.34.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect - google.golang.org/grpc v1.72.1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect + google.golang.org/grpc v1.73.0 // indirect google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/go.sum b/go.sum index 2735878e2..5b943dc60 100644 --- a/go.sum +++ b/go.sum @@ -76,18 +76,18 @@ 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.23.0 h1:MUiBM1s0CNlRFsCLJuM5wXZrzA3MnPYEsiXmzATMW/I= -github.com/hashicorp/terraform-exec v0.23.0/go.mod h1:mA+qnx1R8eePycfwKkCRk3Wy65mwInvlpAeOwmA7vlY= +github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2 h1:90fcAqw0Qmv4vY7zL4jEKgKarHmOnNN6SjTY68eLKGA= +github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2/go.mod h1:8D3RLLpzAZdhT9jvALYz1KHyGU4OvI73I1o0+01QJxA= github.com/hashicorp/terraform-json v0.25.0 h1:rmNqc/CIfcWawGiwXmRuiXJKEiJu1ntGoxseG1hLhoQ= github.com/hashicorp/terraform-json v0.25.0/go.mod h1:sMKS8fiRDX4rVlR6EJUMudg1WcanxCMoWwTLkgZP/vc= -github.com/hashicorp/terraform-plugin-go v0.28.0 h1:zJmu2UDwhVN0J+J20RE5huiF3XXlTYVIleaevHZgKPA= -github.com/hashicorp/terraform-plugin-go v0.28.0/go.mod h1:FDa2Bb3uumkTGSkTFpWSOwWJDwA7bf3vdP3ltLDTH6o= +github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1 h1:ZId6oWG8VTKhz207quE/Xh8a3HuoLtM/QkcSSypekIQ= +github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1/go.mod h1:hL//wLEfYo0YVt0TC/VLzia/ADQQto3HEm4/jX2gkdY= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 h1:NFPMacTrY/IdcIcnUB+7hsore1ZaRWU9cnB6jFoBnIM= github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0/go.mod h1:QYmYnLfsosrxjCnGY1p9c7Zj6n9thnEE+7RObeYs3fA= -github.com/hashicorp/terraform-registry-address v0.2.5 h1:2GTftHqmUhVOeuu9CW3kwDkRe4pcBDq0uuK5VJngU1M= -github.com/hashicorp/terraform-registry-address v0.2.5/go.mod h1:PpzXWINwB5kuVS5CA7m1+eO2f1jKb5ZDIxrOPfpnGkg= +github.com/hashicorp/terraform-registry-address v0.3.0 h1:HMpK3nqaGFPS9VmgRXrJL/dzHNdheGVKk5k7VlFxzCo= +github.com/hashicorp/terraform-registry-address v0.3.0/go.mod h1:jRGCMiLaY9zii3GLC7hqpSnwhfnCN5yzvY0hh4iCGbM= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= @@ -152,16 +152,16 @@ github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6 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.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= -go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= -go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= -go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= -go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= 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.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= @@ -214,10 +214,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T 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-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= -google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= -google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= 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.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= diff --git a/helper/resource/query/examplecloud_test.go b/helper/resource/query/examplecloud_test.go new file mode 100644 index 000000000..1cae8e95d --- /dev/null +++ b/helper/resource/query/examplecloud_test.go @@ -0,0 +1,118 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package query_test + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" +) + +func examplecloudResource() testprovider.Resource { + return testprovider.Resource{ + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "somevalue"), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + }, + )), + }, + ReadResponse: &resource.ReadResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "somevalue"), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + }, + )), + }, + ImportStateResponse: &resource.ImportStateResponse{ + State: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "somevalue"), + }, + ), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + }, + )), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + ComputedStringAttribute("id"), + RequiredStringAttribute("location"), + RequiredStringAttribute("name"), + }, + }, + }, + }, + IdentitySchemaResponse: &resource.IdentitySchemaResponse{ + Schema: &tfprotov6.ResourceIdentitySchema{ + Version: 1, + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + RequiredForImport: true, + }, + }, + }, + }, + } +} diff --git a/helper/resource/query/query_test.go b/helper/resource/query/query_test.go new file mode 100644 index 000000000..4a2c94bb4 --- /dev/null +++ b/helper/resource/query/query_test.go @@ -0,0 +1,63 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package query_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + 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/list" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestQuery(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + ListResources: map[string]testprovider.ListResource{ + "examplecloud_containerette": { + SchemaResponse: &list.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + ComputedStringAttribute("id"), + }, + }, + }, + }, + ListResultsStream: &list.ListResultsStream{ + Results: func(push func(list.ListResult) bool) { + }, + }, + }, + }, + Resources: map[string]testprovider.Resource{ + "examplecloud_containerette": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + Query: true, + Config: ` + provider "examplecloud" {} + list "examplecloud_containerette" "test" { + provider = examplecloud + + config { + id = "bat" + } + }`, + }, + }, + }) +} diff --git a/helper/resource/query/types_test.go b/helper/resource/query/types_test.go new file mode 100644 index 000000000..7620d4d7d --- /dev/null +++ b/helper/resource/query/types_test.go @@ -0,0 +1,66 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package query_test + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func RequiredBoolAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.Bool, + Required: true, + } +} + +func OptionalComputedListAttribute(name string, elementType tftypes.Type) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.List{ElementType: elementType}, + Optional: true, + Computed: true, + } +} + +func RequiredListAttribute(name string, elementType tftypes.Type) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.List{ElementType: elementType}, + Required: true, + } +} + +func RequiredNumberAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.Number, + Required: true, + } +} + +func ComputedStringAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.String, + Computed: true, + } +} + +func OptionalStringAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.String, + Optional: true, + } +} + +func RequiredStringAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.String, + Required: true, + } +} diff --git a/helper/resource/testing.go b/helper/resource/testing.go index c3dd3bdc9..fda33bf7e 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -835,6 +835,9 @@ type TestStep struct { // for performing import testing where the prior TestStep configuration // contained a provider outside the one under test. ExternalProviders map[string]ExternalProvider + + // If true, the test step will run the query command + Query 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 5ae7a5b4d..63c03ca66 100644 --- a/helper/resource/testing_new.go +++ b/helper/resource/testing_new.go @@ -254,7 +254,9 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest testStepConfig = teststep.Configuration(confRequest) - err = wd.SetConfig(ctx, testStepConfig, step.ConfigVariables) + if !step.Query { + err = wd.SetConfig(ctx, testStepConfig, step.ConfigVariables) + } if err != nil { logging.HelperResourceError(ctx, @@ -356,6 +358,39 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest continue } + if step.Query { + logging.HelperResourceTrace(ctx, "TestStep is Query mode") + + queryConfigRequest := teststep.ConfigurationRequest{ + Raw: &step.Config, + } + err := wd.SetQuery(ctx, teststep.Configuration(queryConfigRequest), step.ConfigVariables) + if err != nil { + t.Fatalf("Step %d/%d error setting query: %s", stepNumber, len(c.Steps), err) + } + + err = runProviderCommand(ctx, t, wd, providers, func() error { + return wd.Init(ctx) + }) + if err != nil { + t.Fatalf("Step %d/%d error running init: %s", stepNumber, len(c.Steps), err) + } + + var queryOut []string + err = runProviderCommand(ctx, t, wd, providers, func() error { + var err error + queryOut, err = wd.Query(ctx) + return err + }) + if err != nil { + fmt.Printf("Step %d/%d Query Output:\n%s\n", stepNumber, len(c.Steps), queryOut) + t.Fatalf("Step %d/%d error running query: %s", stepNumber, len(c.Steps), err) + } + + fmt.Printf("Step %d/%d Query Output:\n%s\n", stepNumber, len(c.Steps), queryOut) + continue + } + if cfg != nil { logging.HelperResourceTrace(ctx, "TestStep is Config mode") diff --git a/helper/resource/testing_new_config.go b/helper/resource/testing_new_config.go index babaf8410..7506e65f0 100644 --- a/helper/resource/testing_new_config.go +++ b/helper/resource/testing_new_config.go @@ -435,6 +435,7 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint // this fails. If refresh isn't read-only, then this will have // caught a different bug. if idRefreshCheck != nil { + fmt.Println("Not Writing by testing ID Refresh") if err := testIDRefresh(ctx, t, c, wd, step, idRefreshCheck, providers, stepIndex, helper); err != nil { return fmt.Errorf( "[ERROR] Test: ID-only test failed: %s", err) diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index d29425c32..f7020d439 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -4,6 +4,7 @@ package plugintest import ( + "bytes" "context" "fmt" "io" @@ -21,6 +22,7 @@ import ( const ( ConfigFileName = "terraform_plugin_test.tf" PlanFileName = "tfplan" + QueryFileName = "terraform_plugin_test.tfquery.hcl" ) // WorkingDir represents a distinct working directory that can be used for @@ -37,6 +39,10 @@ type WorkingDir struct { // was stored; empty until SetConfig is called. configFilename string + // queryFilename is the full filename where the latest query configuration + // was stored; empty until SetQuery is called. + queryFilename string + // tf is the instance of tfexec.Terraform used for running Terraform commands tf *tfexec.Terraform @@ -101,7 +107,7 @@ func (wd *WorkingDir) SetConfig(ctx context.Context, cfg teststep.Config, vars c for _, file := range fi { if file.Mode().IsRegular() { - if filepath.Ext(file.Name()) == ".tf" || filepath.Ext(file.Name()) == ".json" { + if filepath.Ext(file.Name()) == ".tf" || filepath.Ext(file.Name()) == ".json" || filepath.Ext(file.Name()) == ".tfquery.hcl" { err = os.Remove(filepath.Join(d.Name(), file.Name())) if err != nil && !os.IsNotExist(err) { @@ -151,6 +157,80 @@ func (wd *WorkingDir) SetConfig(ctx context.Context, cfg teststep.Config, vars c return nil } +// SetQuery sets a new query configuration for the working directory. +// +// This must be called at least once before any call to Init or Query Destroy +// to establish the query configuration. Any previously-set configuration is +// discarded and any saved plan is cleared. +func (wd *WorkingDir) SetQuery(ctx context.Context, cfg teststep.Config, vars config.Variables) error { + // Remove old config and variables files first + d, err := os.Open(wd.baseDir) + + if err != nil { + return err + } + + defer d.Close() + + fi, err := d.Readdir(-1) + + if err != nil { + return err + } + + for _, file := range fi { + if file.Mode().IsRegular() { + if filepath.Ext(file.Name()) == ".warioform" || filepath.Ext(file.Name()) == ".json" || filepath.Ext(file.Name()) == ".tfquery.hcl" { + err = os.Remove(filepath.Join(d.Name(), file.Name())) + + if err != nil && !os.IsNotExist(err) { + return err + } + } + } + } + + logging.HelperResourceTrace(ctx, "Setting Terraform query configuration", map[string]any{logging.KeyTestTerraformConfiguration: cfg}) + + outFilename := filepath.Join(wd.baseDir, QueryFileName) + + // This file has to be written otherwise wd.Init() will return an error. + err = os.WriteFile(outFilename, nil, 0700) + + if err != nil { + return err + } + + // wd.configFilename must be set otherwise wd.Init() will return an error. + wd.queryFilename = outFilename + wd.configFilename = outFilename + + // Write configuration + if cfg != nil { + err = cfg.WriteQuery(ctx, wd.baseDir) + + if err != nil { + return err + } + } + + //Write configuration variables + err = vars.Write(wd.baseDir) + + if err != nil { + return err + } + + // Changing configuration invalidates any saved plan. + err = wd.ClearPlan(ctx) + + if err != nil { + return err + } + + return nil +} + // ClearState deletes any Terraform state present in the working directory. // // Any remote objects tracked by the state are not destroyed first, so this @@ -444,3 +524,21 @@ func (wd *WorkingDir) Schemas(ctx context.Context) (*tfjson.ProviderSchemas, err return providerSchemas, err } + +func (wd *WorkingDir) Query(ctx context.Context) ([]string, error) { + logging.HelperResourceTrace(ctx, "Calling Terraform CLI providers query command") + + // Query the provider using the Terraform CLI function + var buffer bytes.Buffer + err := wd.tf.QueryJSON(context.Background(), &buffer) + + if err != nil { + return nil, fmt.Errorf("error running terraform query command: %w", err) + } + + logging.HelperResourceTrace(ctx, "Called Terraform CLI providers query command") + + output := buffer.String() + + return []string{output}, nil +} diff --git a/internal/testing/testprovider/list_resource.go b/internal/testing/testprovider/list_resource.go new file mode 100644 index 000000000..f57c95a4a --- /dev/null +++ b/internal/testing/testprovider/list_resource.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/list" +) + +var _ list.ListResource = ListResource{} + +type ListResource struct { + SchemaResponse *list.SchemaResponse + ListResultsStream *list.ListResultsStream + ValidateListConfigResponse *list.ValidateListConfigResponse +} + +func (r ListResource) Schema(ctx context.Context, req list.SchemaRequest, resp *list.SchemaResponse) { + if r.SchemaResponse != nil { + resp.Diagnostics = r.SchemaResponse.Diagnostics + resp.Schema = r.SchemaResponse.Schema + } +} +func (r ListResource) List(ctx context.Context, req list.ListRequest, stream *list.ListResultsStream) { + stream.Results = r.ListResultsStream.Results +} diff --git a/internal/testing/testprovider/provider.go b/internal/testing/testprovider/provider.go index 6e7f9d9a7..f30883bcc 100644 --- a/internal/testing/testprovider/provider.go +++ b/internal/testing/testprovider/provider.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov6" "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/provider" "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" ) @@ -19,6 +20,7 @@ var _ provider.Provider = Provider{} type Provider struct { ConfigureResponse *provider.ConfigureResponse DataSources map[string]DataSource + ListResources map[string]ListResource Resources map[string]Resource SchemaResponse *provider.SchemaResponse StopResponse *provider.StopResponse @@ -41,6 +43,16 @@ func (p Provider) DataSourcesMap() map[string]datasource.DataSource { return datasources } +func (p Provider) ListResourcesMap() map[string]list.ListResource { + listResources := make(map[string]list.ListResource, len(p.ListResources)) + + for typeName, d := range p.ListResources { + listResources[typeName] = d + } + + return listResources +} + func (p Provider) ResourcesMap() map[string]resource.Resource { resources := make(map[string]resource.Resource, len(p.Resources)) diff --git a/internal/testing/testsdk/list/list_resource.go b/internal/testing/testsdk/list/list_resource.go new file mode 100644 index 000000000..fb5d7baf7 --- /dev/null +++ b/internal/testing/testsdk/list/list_resource.go @@ -0,0 +1,36 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package list + +import ( + "context" + "iter" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +type ListResource interface { + Schema(context.Context, SchemaRequest, *SchemaResponse) + List(context.Context, ListRequest, *ListResultsStream) +} + +type ListRequest struct { +} + +type ListResultsStream struct { + Results iter.Seq[ListResult] +} + +type ListResult struct { +} + +type ValidateListConfigResponse struct { +} + +type SchemaRequest struct{} + +type SchemaResponse struct { + Diagnostics []*tfprotov6.Diagnostic + Schema *tfprotov6.Schema +} diff --git a/internal/testing/testsdk/provider/provider.go b/internal/testing/testsdk/provider/provider.go index 82c65b9f4..4fcc6b4d3 100644 --- a/internal/testing/testsdk/provider/provider.go +++ b/internal/testing/testsdk/provider/provider.go @@ -9,12 +9,14 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-go/tftypes" "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" ) type Provider interface { Configure(context.Context, ConfigureRequest, *ConfigureResponse) DataSourcesMap() map[string]datasource.DataSource + ListResourcesMap() map[string]list.ListResource ResourcesMap() map[string]resource.Resource Schema(context.Context, SchemaRequest, *SchemaResponse) Stop(context.Context, StopRequest, *StopResponse) diff --git a/internal/testing/testsdk/providerserver/list_resources.go b/internal/testing/testsdk/providerserver/list_resources.go new file mode 100644 index 000000000..cd31ef1e0 --- /dev/null +++ b/internal/testing/testsdk/providerserver/list_resources.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/list" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/provider" +) + +func ProviderListResource(p provider.Provider, typeName string) (list.ListResource, *tfprotov6.Diagnostic) { + r, ok := p.ListResourcesMap()[typeName] + + if !ok { + return nil, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Missing List Resource Type", + Detail: "The provider does not define the list resource type: " + typeName, + } + } + + return r, nil +} diff --git a/internal/testing/testsdk/providerserver/providerserver.go b/internal/testing/testsdk/providerserver/providerserver.go index 0855ff733..a55b5c5c1 100644 --- a/internal/testing/testsdk/providerserver/providerserver.go +++ b/internal/testing/testsdk/providerserver/providerserver.go @@ -12,12 +12,15 @@ import ( "github.com/hashicorp/terraform-plugin-go/tftypes" "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/provider" "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" ) 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) { @@ -86,6 +89,12 @@ func (s ProviderServer) GetMetadata(ctx context.Context, request *tfprotov6.GetM }) } + for typeName := range s.Provider.ListResourcesMap() { + resp.ListResources = append(resp.ListResources, tfprotov6.ListResourceMetadata{ + TypeName: typeName, + }) + } + for typeName := range s.Provider.ResourcesMap() { resp.Resources = append(resp.Resources, tfprotov6.ResourceMetadata{ TypeName: typeName, @@ -304,10 +313,11 @@ func (s ProviderServer) GetProviderSchema(ctx context.Context, req *tfprotov6.Ge Functions: map[string]*tfprotov6.Function{}, EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Diagnostics: providerResp.Diagnostics, - Provider: providerResp.Schema, - ResourceSchemas: map[string]*tfprotov6.Schema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Diagnostics: providerResp.Diagnostics, + ListResourceSchemas: map[string]*tfprotov6.Schema{}, + Provider: providerResp.Schema, + ResourceSchemas: map[string]*tfprotov6.Schema{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ PlanDestroy: true, }, @@ -324,6 +334,17 @@ func (s ProviderServer) GetProviderSchema(ctx context.Context, req *tfprotov6.Ge resp.DataSourceSchemas[typeName] = schemaResp.Schema } + for typeName, l := range s.Provider.ListResourcesMap() { + schemaReq := list.SchemaRequest{} + schemaResp := &list.SchemaResponse{} + + l.Schema(ctx, schemaReq, schemaResp) + + resp.Diagnostics = append(resp.Diagnostics, schemaResp.Diagnostics...) + + resp.ListResourceSchemas[typeName] = schemaResp.Schema + } + for typeName, r := range s.Provider.ResourcesMap() { schemaReq := resource.SchemaRequest{} schemaResp := &resource.SchemaResponse{} @@ -1017,3 +1038,47 @@ func (s ProviderServer) CloseEphemeralResource(ctx context.Context, req *tfproto func (s ProviderServer) ValidateEphemeralResourceConfig(ctx context.Context, req *tfprotov6.ValidateEphemeralResourceConfigRequest) (*tfprotov6.ValidateEphemeralResourceConfigResponse, error) { return &tfprotov6.ValidateEphemeralResourceConfigResponse{}, nil } + +func (s ProviderServer) ListResource(ctx context.Context, req *tfprotov6.ListResourceRequest) (*tfprotov6.ListResourceServerStream, error) { + resp := &tfprotov6.ListResourceServerStream{} + + // Copy over identity if it's supported + identitySchemaReq := resource.IdentitySchemaRequest{} + identitySchemaResp := &resource.IdentitySchemaResponse{} + + r, _ := ProviderResource(s.Provider, req.TypeName) + // TODO: diag + r.IdentitySchema(ctx, identitySchemaReq, identitySchemaResp) + + results := func(push func(tfprotov6.ListResourceResult) bool) { + _, diag := ProviderListResource(s.Provider, req.TypeName) + if diag != nil { + push(tfprotov6.ListResourceResult{Diagnostics: []*tfprotov6.Diagnostic{diag}}) + return + } + + identityData := tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + }, + ) + identity, _ := IdentityValuetoDynamicValue(identitySchemaResp.Schema, identityData) // TODO: diag + push(tfprotov6.ListResourceResult{ + Identity: &tfprotov6.ResourceIdentityData{ + IdentityData: identity, + }, + }) + } + + resp.Results = results + return resp, nil +} + +func (s ProviderServer) ValidateListResourceConfig(ctx context.Context, req *tfprotov6.ValidateListResourceConfigRequest) (*tfprotov6.ValidateListResourceConfigResponse, error) { + return &tfprotov6.ValidateListResourceConfigResponse{}, nil +} diff --git a/internal/testing/testsdk/resource/resource.go b/internal/testing/testsdk/resource/resource.go index 3fb3703ae..dc6412d55 100644 --- a/internal/testing/testsdk/resource/resource.go +++ b/internal/testing/testsdk/resource/resource.go @@ -5,7 +5,6 @@ package resource import ( "context" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-go/tftypes" ) diff --git a/internal/teststep/config.go b/internal/teststep/config.go index 91a708e26..5df477f08 100644 --- a/internal/teststep/config.go +++ b/internal/teststep/config.go @@ -16,8 +16,9 @@ import ( ) const ( - rawConfigFileName = "terraform_plugin_test.tf" - rawConfigFileNameJSON = rawConfigFileName + ".json" + rawConfigFileName = "terraform_plugin_test.tf" + rawConfigFileNameJSON = rawConfigFileName + ".json" + rawQueryConfigFileName = "terraform_plugin_test.tfquery.hcl" ) var ( @@ -46,6 +47,7 @@ type Config interface { HasTerraformBlock(context.Context) (bool, error) Write(context.Context, string) error Append(string) Config + WriteQuery(context.Context, string) error } // PrepareConfigurationRequest is used to simplify the generation of diff --git a/internal/teststep/directory.go b/internal/teststep/directory.go index 67ecc5ccd..1afc45a2d 100644 --- a/internal/teststep/directory.go +++ b/internal/teststep/directory.go @@ -75,6 +75,10 @@ func (c configurationDirectory) HasTerraformBlock(ctx context.Context) (bool, er return contains, nil } +func (c configurationDirectory) WriteQuery(ctx context.Context, dest string) error { + panic("WriteQuery not supported for configurationDirectory") +} + // Write copies all files from directory to destination. func (c configurationDirectory) Write(ctx context.Context, dest string) error { configDirectory := c.directory diff --git a/internal/teststep/file.go b/internal/teststep/file.go index 75ee6f7d6..a4862b05d 100644 --- a/internal/teststep/file.go +++ b/internal/teststep/file.go @@ -71,6 +71,10 @@ func (c configurationFile) HasTerraformBlock(ctx context.Context) (bool, error) return contains, nil } +func (c configurationFile) WriteQuery(ctx context.Context, dest string) error { + panic("WriteQuery not supported for configurationFile") +} + // Write copies file from c.file to destination. func (c configurationFile) Write(ctx context.Context, dest string) error { configFile := c.file diff --git a/internal/teststep/string.go b/internal/teststep/string.go index 39028682a..ccb8fb3e8 100644 --- a/internal/teststep/string.go +++ b/internal/teststep/string.go @@ -61,6 +61,20 @@ func (c configurationString) Write(ctx context.Context, dest string) error { return nil } +// WriteQuery creates a file and writes c.raw into it. +func (c configurationString) WriteQuery(ctx context.Context, dest string) error { + outFilename := filepath.Join(dest, rawQueryConfigFileName) + + bCfg := []byte(c.raw) + + err := os.WriteFile(outFilename, bCfg, 0700) + if err != nil { + return err + } + + return nil +} + func (c configurationString) Append(config string) Config { return configurationString{ raw: strings.Join([]string{c.raw, config}, "\n"), diff --git a/tfversion/versions.go b/tfversion/versions.go index ffb625c8d..dc5cc7dfe 100644 --- a/tfversion/versions.go +++ b/tfversion/versions.go @@ -39,4 +39,6 @@ var ( Version1_10_0 *version.Version = version.Must(version.NewVersion("1.10.0")) Version1_11_0 *version.Version = version.Must(version.NewVersion("1.11.0")) 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")) )