diff --git a/go.mod b/go.mod index b9c07d8cd..a6620a6ce 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/hashicorp/logutils v1.0.0 github.com/hashicorp/terraform-exec v0.23.0 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 @@ -50,13 +50,13 @@ require ( github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect golang.org/x/mod v0.25.0 // indirect - golang.org/x/net v0.40.0 // indirect + golang.org/x/net v0.41.0 // indirect golang.org/x/sync v0.15.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.26.0 // indirect golang.org/x/tools v0.33.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 2d4c29198..c993fb1ad 100644 --- a/go.sum +++ b/go.sum @@ -82,12 +82,15 @@ github.com/hashicorp/terraform-json v0.25.0 h1:rmNqc/CIfcWawGiwXmRuiXJKEiJu1ntGo 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/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= @@ -175,6 +178,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -216,8 +220,10 @@ google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAs 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/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +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..167b30515 --- /dev/null +++ b/helper/resource/query/query_test.go @@ -0,0 +1,60 @@ +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_13_0), // Query mode requires Terraform 1.13.0 or later + }, + 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..f35c9a3aa 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -835,6 +835,8 @@ type TestStep struct { // for performing import testing where the prior TestStep configuration // contained a provider outside the one under test. ExternalProviders map[string]ExternalProvider + + 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..479484513 100644 --- a/helper/resource/testing_new.go +++ b/helper/resource/testing_new.go @@ -106,6 +106,7 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest }, ) + fmt.Println("Writing provider configuration:", c.providerConfig(ctx, false)) err := wd.SetConfig(ctx, config, nil) if err != nil { @@ -254,7 +255,10 @@ 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 { + fmt.Println("Writing pre-switch configuration:", rawCfg) + err = wd.SetConfig(ctx, testStepConfig, step.ConfigVariables) + } if err != nil { logging.HelperResourceError(ctx, @@ -356,6 +360,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") @@ -567,6 +604,7 @@ func testIDRefresh(ctx context.Context, t testing.T, c TestCase, wd *plugintest. testStepConfigDefer := teststep.Configuration(confRequest) + fmt.Println("Writing the reset to original configuration:", rawCfg) err = wd.SetConfig(ctx, testStepConfigDefer, step.ConfigVariables) if err != nil { 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..cbab26e7a 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -1,13 +1,16 @@ -// Copyright (c) HashiCorp, Inc. +//ng Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package plugintest import ( + "bytes" "context" + "encoding/json" "fmt" "io" "os" + "os/exec" "path/filepath" "github.com/hashicorp/terraform-exec/tfexec" @@ -21,6 +24,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 +41,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 +109,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()) == ".hcl" { err = os.Remove(filepath.Join(d.Name(), file.Name())) if err != nil && !os.IsNotExist(err) { @@ -151,6 +159,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()) == ".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 +526,38 @@ 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") + + var reattachStr string + if wd.reattachInfo != nil { + reattachJson, err := json.Marshal(wd.reattachInfo) + if err != nil { + return nil, fmt.Errorf("error marshaling reattach info: %w", err) + } + reattachStr = string(reattachJson) + } + + buffer := &bytes.Buffer{} + errBuffer := &bytes.Buffer{} + cmd := exec.CommandContext(ctx, wd.terraformExec, "query", "-json") + cmd.Dir = wd.baseDir + cmd.Stdout = buffer + cmd.Stderr = errBuffer + cmd.Env = os.Environ() + fmt.Println("Reattach info: ", reattachStr) + cmd.Env = append(cmd.Env, "TF_REATTACH_PROVIDERS="+reattachStr) + + err := cmd.Run() + if err != nil { + output := buffer.String() + "\n\n" + errBuffer.String() + fmt.Println(output) + 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..2f3d61af0 --- /dev/null +++ b/internal/testing/testsdk/list/list_resource.go @@ -0,0 +1,33 @@ +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..1b339adc7 100644 --- a/internal/testing/testsdk/providerserver/providerserver.go +++ b/internal/testing/testsdk/providerserver/providerserver.go @@ -12,11 +12,13 @@ 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. @@ -86,6 +88,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 +312,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 +333,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 +1037,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/teststep/config.go b/internal/teststep/config.go index 91a708e26..261fd7795 100644 --- a/internal/teststep/config.go +++ b/internal/teststep/config.go @@ -18,6 +18,9 @@ import ( const ( rawConfigFileName = "terraform_plugin_test.tf" rawConfigFileNameJSON = rawConfigFileName + ".json" + + rawQueryConfigFileName = "terraform_plugin_test.tfquery.hcl" + // rawQueryConfigFileNameJSON = rawConfigFileName + ".json" ) var ( @@ -45,6 +48,7 @@ type Config interface { HasProviderBlock(context.Context) (bool, error) HasTerraformBlock(context.Context) (bool, error) Write(context.Context, string) error + WriteQuery(context.Context, string) error Append(string) Config } 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..76130f650 100644 --- a/tfversion/versions.go +++ b/tfversion/versions.go @@ -39,4 +39,5 @@ 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")) )