diff --git a/commands.go b/commands.go index 79424845043b..b8489519b40b 100644 --- a/commands.go +++ b/commands.go @@ -276,6 +276,12 @@ func initCommands( }, nil }, + "query": func() (cli.Command, error) { + return &command.QueryCommand{ + Meta: meta, + }, nil + }, + "refresh": func() (cli.Command, error) { return &command.RefreshCommand{ Meta: meta, @@ -451,12 +457,6 @@ func initCommands( }, nil } - Commands["query"] = func() (cli.Command, error) { - return &command.QueryCommand{ - Meta: meta, - }, nil - } - Commands["test cleanup"] = func() (cli.Command, error) { return &command.TestCleanupCommand{ Meta: meta, diff --git a/internal/cloud/backend_query.go b/internal/cloud/backend_query.go index 3fa78d4bfa8f..a8759c5f5cb9 100644 --- a/internal/cloud/backend_query.go +++ b/internal/cloud/backend_query.go @@ -20,6 +20,7 @@ import ( "github.com/hashicorp/terraform/internal/genconfig" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" ctyjson "github.com/zclconf/go-cty/cty/json" ) @@ -276,7 +277,7 @@ func (b *Cloud) cancelQueryRun(cancelCtx context.Context, op *backendrun.Operati // formatIdentity formats the identity map into a string representation. // It flattens the map into a string of key=value pairs, separated by commas. func formatIdentity(identity map[string]json.RawMessage) string { - parts := make([]string, 0, len(identity)) + ctyObj := make(map[string]cty.Value, len(identity)) for key, value := range identity { ty, err := ctyjson.ImpliedType(value) if err != nil { @@ -286,9 +287,9 @@ func formatIdentity(identity map[string]json.RawMessage) string { if err != nil { continue } - parts = append(parts, fmt.Sprintf("%s=%s", key, tfdiags.ValueToString(v))) + ctyObj[key] = v } - return strings.Join(parts, ",") + return tfdiags.ObjectToString(cty.ObjectVal(ctyObj)) } const queryDefaultHeader = ` diff --git a/internal/cloud/backend_query_test.go b/internal/cloud/backend_query_test.go index 31fab4db2402..83feb260a6dc 100644 --- a/internal/cloud/backend_query_test.go +++ b/internal/cloud/backend_query_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/google/go-cmp/cmp" "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/backend/backendrun" @@ -121,8 +122,15 @@ func TestCloud_queryJSONBasic(t *testing.T) { outp := close(t) gotOut := outp.Stdout() - if !strings.Contains(gotOut, "list.concept_pet.pets id=complete-gannet,legs=6 This is a complete-gannet") { - t.Fatalf("expected query results in output: %s", gotOut) + expectedOut := `list.concept_pet.pets id=large-roughy,legs=2 This is a large-roughy +list.concept_pet.pets id=able-werewolf,legs=5 This is a able-werewolf +list.concept_pet.pets id=complete-gannet,legs=6 This is a complete-gannet +list.concept_pet.pets id=charming-beagle,legs=3 This is a charming-beagle +list.concept_pet.pets id=legal-lamprey,legs=2 This is a legal-lamprey + +` + if diff := cmp.Diff(expectedOut, gotOut); diff != "" { + t.Fatalf("expected query results output to be %s, got %s: diff: %s", expectedOut, gotOut, diff) } stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName) diff --git a/internal/cloud/testdata/query-json-basic/main.tfquery.hcl b/internal/cloud/testdata/query-json-basic/main.tfquery.hcl index 4652ae7d19da..a3276bd012a1 100644 --- a/internal/cloud/testdata/query-json-basic/main.tfquery.hcl +++ b/internal/cloud/testdata/query-json-basic/main.tfquery.hcl @@ -1 +1,3 @@ -list "concept_pet" "pets" {} +list "concept_pet" "pets" { + provider = concept +} diff --git a/internal/cloud/testdata/query/main.tfquery.hcl b/internal/cloud/testdata/query/main.tfquery.hcl index f8c5bb1924c3..58f63c04942e 100644 --- a/internal/cloud/testdata/query/main.tfquery.hcl +++ b/internal/cloud/testdata/query/main.tfquery.hcl @@ -1 +1,3 @@ -list "null_resource" "foo" {} +list "null_resource" "foo" { + provider = null +} diff --git a/internal/command/e2etest/providers_schema_test.go b/internal/command/e2etest/providers_schema_test.go index 7a5d7dea8d47..582a21710c21 100644 --- a/internal/command/e2etest/providers_schema_test.go +++ b/internal/command/e2etest/providers_schema_test.go @@ -134,6 +134,21 @@ func TestProvidersSchema(t *testing.T) { } } }, + "list_resource_schemas": { + "simple_resource": { + "version": 0, + "block": { + "attributes": { + "value": { + "type": "string", + "description_kind": "plain", + "optional": true + } + }, + "description_kind": "plain" + } + } + }, "resource_identity_schemas": { "simple_resource": { "version": 0, @@ -213,6 +228,21 @@ func TestProvidersSchema(t *testing.T) { } } }, + "list_resource_schemas": { + "simple_resource": { + "version": 0, + "block": { + "attributes": { + "value": { + "type": "string", + "description_kind": "plain", + "optional": true + } + }, + "description_kind": "plain" + } + } + }, "functions": { "noop": { "description": "noop takes any single argument and returns the same value", diff --git a/internal/command/jsonformat/plan_test.go b/internal/command/jsonformat/plan_test.go index f971c08d02af..5f99c2fd9e7e 100644 --- a/internal/command/jsonformat/plan_test.go +++ b/internal/command/jsonformat/plan_test.go @@ -8071,7 +8071,7 @@ func runTestCases(t *testing.T, testCases map[string]testCase) { return } - jsonschemas := jsonprovider.MarshalForRenderer(tfschemas, false) + jsonschemas := jsonprovider.MarshalForRenderer(tfschemas) change := structured.FromJsonChange(jsonchanges[0].Change, attribute_path.AlwaysMatcher()) renderer := Renderer{Colorize: color} diff := diff{ @@ -8421,7 +8421,7 @@ func TestResourceChange_deferredActions(t *testing.T) { } renderer := Renderer{Colorize: color} - jsonschemas := jsonprovider.MarshalForRenderer(fullSchema, false) + jsonschemas := jsonprovider.MarshalForRenderer(fullSchema) diffs := precomputeDiffs(Plan{ DeferredChanges: deferredChanges, ProviderSchemas: jsonschemas, @@ -8714,7 +8714,7 @@ func TestResourceChange_actions(t *testing.T) { }, }, } - jsonschemas := jsonprovider.MarshalForRenderer(fullSchema, false) + jsonschemas := jsonprovider.MarshalForRenderer(fullSchema) diffs := precomputeDiffs(Plan{ ResourceChanges: []jsonplan.ResourceChange{defaultResourceChange}, ActionInvocations: tc.actionInvocations, diff --git a/internal/command/jsonformat/state_test.go b/internal/command/jsonformat/state_test.go index 9c4eafa50b23..b8350f29bbbb 100644 --- a/internal/command/jsonformat/state_test.go +++ b/internal/command/jsonformat/state_test.go @@ -86,7 +86,7 @@ func TestState(t *testing.T) { RootModule: root, RootModuleOutputs: outputs, ProviderFormatVersion: jsonprovider.FormatVersion, - ProviderSchemas: jsonprovider.MarshalForRenderer(tt.Schemas, false), + ProviderSchemas: jsonprovider.MarshalForRenderer(tt.Schemas), }) result := done(t).All() diff --git a/internal/command/jsonprovider/provider.go b/internal/command/jsonprovider/provider.go index 0ebb76f82b91..10fdcf7831f4 100644 --- a/internal/command/jsonprovider/provider.go +++ b/internal/command/jsonprovider/provider.go @@ -45,22 +45,22 @@ func newProviders() *Providers { // schema into the public structured JSON versions. // // This is a format that can be read by the structured plan renderer. -func MarshalForRenderer(s *terraform.Schemas, includeExperimentalSchemas bool) map[string]*Provider { +func MarshalForRenderer(s *terraform.Schemas) map[string]*Provider { schemas := make(map[string]*Provider, len(s.Providers)) for k, v := range s.Providers { - schemas[k.String()] = marshalProvider(v, includeExperimentalSchemas) + schemas[k.String()] = marshalProvider(v) } return schemas } -func Marshal(s *terraform.Schemas, includeExperimentalSchemas bool) ([]byte, error) { +func Marshal(s *terraform.Schemas) ([]byte, error) { providers := newProviders() - providers.Schemas = MarshalForRenderer(s, includeExperimentalSchemas) + providers.Schemas = MarshalForRenderer(s) ret, err := json.Marshal(providers) return ret, err } -func marshalProvider(tps providers.ProviderSchema, includeExperimentalSchemas bool) *Provider { +func marshalProvider(tps providers.ProviderSchema) *Provider { p := &Provider{ Provider: marshalSchema(tps.Provider), ResourceSchemas: marshalSchemas(tps.ResourceTypes), @@ -71,20 +71,18 @@ func marshalProvider(tps providers.ProviderSchema, includeExperimentalSchemas bo ActionSchemas: marshalActionSchemas(tps.Actions), } - if includeExperimentalSchemas { - // List resource schemas are nested under a "config" block, so we need to - // extract that block to get the actual provider schema for the list resource. - // When getting the provider schemas, Terraform adds this extra level to - // better match the actual configuration structure. - listSchemas := make(map[string]providers.Schema, len(tps.ListResourceTypes)) - for k, v := range tps.ListResourceTypes { - listSchemas[k] = providers.Schema{ - Body: &v.Body.BlockTypes["config"].Block, - Version: v.Version, - } + // List resource schemas are nested under a "config" block, so we need to + // extract that block to get the actual provider schema for the list resource. + // When getting the provider schemas, Terraform adds this extra level to + // better match the actual configuration structure. + listSchemas := make(map[string]providers.Schema, len(tps.ListResourceTypes)) + for k, v := range tps.ListResourceTypes { + listSchemas[k] = providers.Schema{ + Body: &v.Body.BlockTypes["config"].Block, + Version: v.Version, } - p.ListResourceSchemas = marshalSchemas(listSchemas) } + p.ListResourceSchemas = marshalSchemas(listSchemas) return p } diff --git a/internal/command/jsonprovider/provider_test.go b/internal/command/jsonprovider/provider_test.go index e65e4867869b..bb9ca97486b1 100644 --- a/internal/command/jsonprovider/provider_test.go +++ b/internal/command/jsonprovider/provider_test.go @@ -20,25 +20,23 @@ var cmpOpts = cmpopts.IgnoreUnexported(Provider{}) func TestMarshalProvider(t *testing.T) { tests := []struct { - Input providers.ProviderSchema - IncludeExperimental bool - Want *Provider + Input providers.ProviderSchema + Want *Provider }{ { providers.ProviderSchema{}, - false, &Provider{ Provider: &Schema{}, ResourceSchemas: map[string]*Schema{}, DataSourceSchemas: map[string]*Schema{}, EphemeralResourceSchemas: map[string]*Schema{}, ResourceIdentitySchemas: map[string]*IdentitySchema{}, + ListResourceSchemas: map[string]*Schema{}, ActionSchemas: map[string]*ActionSchema{}, }, }, { testProvider(), - false, &Provider{ Provider: &Schema{ Block: &Block{ @@ -212,53 +210,6 @@ func TestMarshalProvider(t *testing.T) { }, }, }, - ResourceIdentitySchemas: map[string]*IdentitySchema{}, - ActionSchemas: map[string]*ActionSchema{}, - }, - }, - { - providers.ProviderSchema{ - ListResourceTypes: map[string]providers.Schema{ - "test_list_resource": { - Version: 1, - Body: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "data": { - Type: cty.DynamicPseudoType, - Computed: true, - }, - }, - BlockTypes: map[string]*configschema.NestedBlock{ - "config": { - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "filter": {Type: cty.String, Optional: true}, - "items": {Type: cty.List(cty.String), Required: true}, - }, - }, - Nesting: configschema.NestingSingle, - }, - }, - }, - }, - }, - Actions: map[string]providers.ActionSchema{ - "test_action": { - ConfigSchema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "opt_attr": {Type: cty.String, Optional: true}, - "req_attr": {Type: cty.List(cty.String), Required: true}, - }, - }, - }, - }, - }, - true, - &Provider{ - Provider: &Schema{}, - ResourceSchemas: map[string]*Schema{}, - DataSourceSchemas: map[string]*Schema{}, - EphemeralResourceSchemas: map[string]*Schema{}, ListResourceSchemas: map[string]*Schema{ "test_list_resource": { Version: 1, @@ -305,7 +256,7 @@ func TestMarshalProvider(t *testing.T) { for i, test := range tests { t.Run(fmt.Sprint(i), func(t *testing.T) { - got := marshalProvider(test.Input, test.IncludeExperimental) + got := marshalProvider(test.Input) if diff := cmp.Diff(test.Want, got, cmpOpts); diff != "" { t.Fatalf("wrong result:\n %s\n", diff) } @@ -431,5 +382,15 @@ func testProvider() providers.ProviderSchema { }, }, }, + Actions: map[string]providers.ActionSchema{ + "test_action": { + ConfigSchema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "opt_attr": {Type: cty.String, Optional: true}, + "req_attr": {Type: cty.List(cty.String), Required: true}, + }, + }, + }, + }, } } diff --git a/internal/command/meta.go b/internal/command/meta.go index 0dc89ecdd0d8..1626109fbfb4 100644 --- a/internal/command/meta.go +++ b/internal/command/meta.go @@ -272,6 +272,9 @@ type Meta struct { // Used with commands which write state to allow users to write remote // state even if the remote and local Terraform versions don't match. ignoreRemoteVersion bool + + // set to true if query files should be parsed + includeQueryFiles bool } type testingOverrides struct { diff --git a/internal/command/meta_config.go b/internal/command/meta_config.go index 3887d77db420..6485a4347916 100644 --- a/internal/command/meta_config.go +++ b/internal/command/meta_config.go @@ -38,7 +38,7 @@ func (m *Meta) normalizePath(path string) string { // loadConfig reads a configuration from the given directory, which should // contain a root module and have already have any required descendant modules // installed. -func (m *Meta) loadConfig(rootDir string, parserOpts ...configs.Option) (*configs.Config, tfdiags.Diagnostics) { +func (m *Meta) loadConfig(rootDir string) (*configs.Config, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics rootDir = m.normalizePath(rootDir) @@ -48,7 +48,7 @@ func (m *Meta) loadConfig(rootDir string, parserOpts ...configs.Option) (*config return nil, diags } - config, hclDiags := loader.LoadConfig(rootDir, parserOpts...) + config, hclDiags := loader.LoadConfig(rootDir) diags = diags.Append(hclDiags) return config, diags } @@ -353,8 +353,9 @@ func (m *Meta) registerSynthConfigSource(filename string, src []byte) { func (m *Meta) initConfigLoader() (*configload.Loader, error) { if m.configLoader == nil { loader, err := configload.NewLoader(&configload.Config{ - ModulesDir: m.modulesDir(), - Services: m.Services, + ModulesDir: m.modulesDir(), + Services: m.Services, + IncludeQueryFiles: m.includeQueryFiles, }) if err != nil { return nil, err diff --git a/internal/command/plan_test.go b/internal/command/plan_test.go index e77ae0eb6ce6..b3e726c8c00f 100644 --- a/internal/command/plan_test.go +++ b/internal/command/plan_test.go @@ -1588,6 +1588,56 @@ func TestPlan_jsonGoldenReference(t *testing.T) { checkGoldenReference(t, output, "plan") } +// Tests related to how plan command behaves when there are query files in the configuration path +func TestPlan_QueryFiles(t *testing.T) { + // a plan succeeds regardless of valid or invalid + // tfquery files in the configuration path + t.Run("with invalid query files in the config path", func(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("query/invalid-syntax"), td) + t.Chdir(td) + + p := planFixtureProvider() + view, done := testView(t) + c := &PlanCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + View: view, + }, + } + + args := []string{} + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) + } + }) + + // the duplicate in the query should not matter because query files are not processed + t.Run("with duplicate variables across query and plan file", func(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("query/duplicate-variables"), td) + t.Chdir(td) + + p := planFixtureProvider() + view, done := testView(t) + c := &PlanCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + View: view, + }, + } + + args := []string{"-var", "instance_name=foo"} + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) + } + }) +} + // planFixtureSchema returns a schema suitable for processing the // configuration in testdata/plan . This schema should be // assigned to a mock provider named "test". diff --git a/internal/command/providers_schema.go b/internal/command/providers_schema.go index 0d51769133d3..919e1a57078c 100644 --- a/internal/command/providers_schema.go +++ b/internal/command/providers_schema.go @@ -107,7 +107,7 @@ func (c *ProvidersSchemaCommand) Run(args []string) int { return 1 } - jsonSchemas, err := jsonprovider.Marshal(schemas, c.AllowExperimentalFeatures) + jsonSchemas, err := jsonprovider.Marshal(schemas) if err != nil { c.Ui.Error(fmt.Sprintf("Failed to marshal provider schemas to json: %s", err)) return 1 diff --git a/internal/command/query.go b/internal/command/query.go index 6e1d00d73097..5faf2f0616d8 100644 --- a/internal/command/query.go +++ b/internal/command/query.go @@ -75,6 +75,7 @@ func (c *QueryCommand) Run(rawArgs []string) int { // migrated to views. c.Meta.color = !common.NoColor c.Meta.Color = c.Meta.color + c.Meta.includeQueryFiles = true // Parse and validate flags args, diags := arguments.ParseQuery(rawArgs) diff --git a/internal/command/query_test.go b/internal/command/query_test.go index 864194988f5e..36e048456d3d 100644 --- a/internal/command/query_test.go +++ b/internal/command/query_test.go @@ -80,7 +80,7 @@ configuration file (.tfquery.hcl file) and try again. name: "invalid query syntax", directory: "invalid-syntax", expectedOut: "", - initCode: 1, + initCode: 0, expectedErr: []string{` Error: Unsupported block type @@ -173,21 +173,6 @@ value. Use a -var or -var-file command line argument to provide a value for this variable. `}, }, - { - name: "error - duplicate variable across .tf and .tfquery files", - directory: "duplicate-variables", - expectedOut: "", - expectedErr: []string{` -Error: Duplicate variable declaration - - on query.tfquery.hcl line 2: - 2: variable "instance_name" { - -A variable named "instance_name" was already declared at main.tf:15,1-25. -Variable names must be unique within a module. -`}, - initCode: 1, - }, } for _, ts := range tests { diff --git a/internal/command/state_show.go b/internal/command/state_show.go index 4ef3bd80977c..b76d398ccd45 100644 --- a/internal/command/state_show.go +++ b/internal/command/state_show.go @@ -155,7 +155,7 @@ func (c *StateShowCommand) Run(args []string) int { ProviderFormatVersion: jsonprovider.FormatVersion, RootModule: root, RootModuleOutputs: outputs, - ProviderSchemas: jsonprovider.MarshalForRenderer(schemas, false), + ProviderSchemas: jsonprovider.MarshalForRenderer(schemas), } renderer := jsonformat.Renderer{ diff --git a/internal/command/views/operation.go b/internal/command/views/operation.go index ae9beca1b190..3b132991f9fa 100644 --- a/internal/command/views/operation.go +++ b/internal/command/views/operation.go @@ -110,7 +110,7 @@ func (v *OperationHuman) Plan(plan *plans.Plan, schemas *terraform.Schemas) { OutputChanges: outputs, ResourceChanges: changed, ResourceDrift: drift, - ProviderSchemas: jsonprovider.MarshalForRenderer(schemas, false), + ProviderSchemas: jsonprovider.MarshalForRenderer(schemas), RelevantAttributes: attrs, ActionInvocations: actions, } diff --git a/internal/command/views/show.go b/internal/command/views/show.go index 50429aa315f8..7eb76bf17e3d 100644 --- a/internal/command/views/show.go +++ b/internal/command/views/show.go @@ -83,7 +83,7 @@ func (v *ShowHuman) Display(config *configs.Config, plan *plans.Plan, planJSON * OutputChanges: outputs, ResourceChanges: changed, ResourceDrift: drift, - ProviderSchemas: jsonprovider.MarshalForRenderer(schemas, false), + ProviderSchemas: jsonprovider.MarshalForRenderer(schemas), RelevantAttributes: attrs, ActionInvocations: actions, } @@ -113,7 +113,7 @@ func (v *ShowHuman) Display(config *configs.Config, plan *plans.Plan, planJSON * ProviderFormatVersion: jsonprovider.FormatVersion, RootModule: root, RootModuleOutputs: outputs, - ProviderSchemas: jsonprovider.MarshalForRenderer(schemas, false), + ProviderSchemas: jsonprovider.MarshalForRenderer(schemas), } renderer.RenderHumanState(jstate) diff --git a/internal/command/views/test.go b/internal/command/views/test.go index 0feb2bab780d..e95e6afdc62a 100644 --- a/internal/command/views/test.go +++ b/internal/command/views/test.go @@ -203,7 +203,7 @@ func (t *TestHuman) Run(run *moduletest.Run, file *moduletest.File, progress mod ProviderFormatVersion: jsonprovider.FormatVersion, RootModule: root, RootModuleOutputs: outputs, - ProviderSchemas: jsonprovider.MarshalForRenderer(schemas, false), + ProviderSchemas: jsonprovider.MarshalForRenderer(schemas), } t.view.streams.Println() // Separate the state from any previous statements. @@ -225,7 +225,7 @@ func (t *TestHuman) Run(run *moduletest.Run, file *moduletest.File, progress mod OutputChanges: outputs, ResourceChanges: changed, ResourceDrift: drift, - ProviderSchemas: jsonprovider.MarshalForRenderer(schemas, false), + ProviderSchemas: jsonprovider.MarshalForRenderer(schemas), RelevantAttributes: attrs, ActionInvocations: actions, } @@ -568,7 +568,7 @@ func (t *TestJSON) Run(run *moduletest.Run, file *moduletest.File, progress modu ProviderFormatVersion: jsonprovider.FormatVersion, RootModule: root, RootModuleOutputs: outputs, - ProviderSchemas: jsonprovider.MarshalForRenderer(schemas, false), + ProviderSchemas: jsonprovider.MarshalForRenderer(schemas), } t.view.log.Info( @@ -592,7 +592,7 @@ func (t *TestJSON) Run(run *moduletest.Run, file *moduletest.File, progress modu OutputChanges: outputs, ResourceChanges: changed, ResourceDrift: drift, - ProviderSchemas: jsonprovider.MarshalForRenderer(schemas, false), + ProviderSchemas: jsonprovider.MarshalForRenderer(schemas), RelevantAttributes: attrs, ActionInvocations: actions, } diff --git a/internal/configs/config_build_test.go b/internal/configs/config_build_test.go index 496d03b2492b..1537de6e62cc 100644 --- a/internal/configs/config_build_test.go +++ b/internal/configs/config_build_test.go @@ -227,7 +227,12 @@ func TestBuildConfigInvalidModules(t *testing.T) { path := filepath.Join(testDir, name) parser.AllowLanguageExperiments(true) - mod, diags := parser.LoadConfigDirWithTests(path, "tests") + opts := []Option{MatchTestFiles("tests")} + if name == "list-in-child-module" { + opts = append(opts, MatchQueryFiles()) + } + + mod, diags := parser.LoadConfigDir(path, opts...) if diags.HasErrors() { // these tests should only trigger errors that are caught in // the config loader. @@ -265,7 +270,7 @@ func TestBuildConfigInvalidModules(t *testing.T) { // for simplicity, these tests will treat all source // addresses as relative to the root module sourcePath := filepath.Join(path, req.SourceAddr.String()) - mod, diags := parser.LoadConfigDir(sourcePath) + mod, diags := parser.LoadConfigDir(sourcePath, opts...) version, _ := version.NewVersion("1.0.0") return mod, version, diags }), diff --git a/internal/configs/configload/loader.go b/internal/configs/configload/loader.go index 07d68b30dd7e..07dfff0db6dd 100644 --- a/internal/configs/configload/loader.go +++ b/internal/configs/configload/loader.go @@ -27,6 +27,8 @@ type Loader struct { // modules is used to install and locate descendant modules that are // referenced (directly or indirectly) from the root module. modules moduleMgr + + parserOpts []configs.Option } // Config is used with NewLoader to specify configuration arguments for the @@ -43,6 +45,10 @@ type Config struct { // not supported, which should be true only in specialized circumstances // such as in tests. Services *disco.Disco + + // IncludeQueryFiles is set to true if query files should be parsed + // when running query commands. + IncludeQueryFiles bool } // NewLoader creates and returns a loader that reads configuration from the @@ -65,6 +71,7 @@ func NewLoader(config *Config) (*Loader, error) { Services: config.Services, Registry: reg, }, + parserOpts: make([]configs.Option, 0), } err := ret.modules.readModuleManifestSnapshot() @@ -72,6 +79,10 @@ func NewLoader(config *Config) (*Loader, error) { return nil, fmt.Errorf("failed to read module manifest: %s", err) } + if config.IncludeQueryFiles { + ret.parserOpts = append(ret.parserOpts, configs.MatchQueryFiles()) + } + return ret, nil } @@ -122,7 +133,7 @@ func (l *Loader) Sources() map[string][]byte { // least one Terraform configuration file. This is a wrapper around calling // the same method name on the loader's parser. func (l *Loader) IsConfigDir(path string) bool { - return l.parser.IsConfigDir(path) + return l.parser.IsConfigDir(path, l.parserOpts...) } // ImportSources writes into the receiver's source code map the given source diff --git a/internal/configs/configload/loader_load.go b/internal/configs/configload/loader_load.go index f7b776b86082..3b9407a32b6c 100644 --- a/internal/configs/configload/loader_load.go +++ b/internal/configs/configload/loader_load.go @@ -22,14 +22,14 @@ import ( // // LoadConfig performs the basic syntax and uniqueness validations that are // required to process the individual modules -func (l *Loader) LoadConfig(rootDir string, parserOpts ...configs.Option) (*configs.Config, hcl.Diagnostics) { - return l.loadConfig(l.parser.LoadConfigDir(rootDir, parserOpts...)) +func (l *Loader) LoadConfig(rootDir string) (*configs.Config, hcl.Diagnostics) { + return l.loadConfig(l.parser.LoadConfigDir(rootDir, l.parserOpts...)) } // LoadConfigWithTests matches LoadConfig, except the configs.Config contains // any relevant .tftest.hcl files. func (l *Loader) LoadConfigWithTests(rootDir string, testDir string) (*configs.Config, hcl.Diagnostics) { - return l.loadConfig(l.parser.LoadConfigDirWithTests(rootDir, testDir)) + return l.loadConfig(l.parser.LoadConfigDir(rootDir, append(l.parserOpts, configs.MatchTestFiles(testDir))...)) } func (l *Loader) loadConfig(rootMod *configs.Module, diags hcl.Diagnostics) (*configs.Config, hcl.Diagnostics) { diff --git a/internal/configs/configload/loader_snapshot.go b/internal/configs/configload/loader_snapshot.go index d8260cddd7ca..46efab362c6e 100644 --- a/internal/configs/configload/loader_snapshot.go +++ b/internal/configs/configload/loader_snapshot.go @@ -24,7 +24,7 @@ import ( // creates an in-memory snapshot of the configuration files used, which can // be later used to create a loader that may read only from this snapshot. func (l *Loader) LoadConfigWithSnapshot(rootDir string) (*configs.Config, *Snapshot, hcl.Diagnostics) { - rootMod, diags := l.parser.LoadConfigDir(rootDir) + rootMod, diags := l.parser.LoadConfigDir(rootDir, l.parserOpts...) if rootMod == nil { return nil, nil, diags } diff --git a/internal/configs/configload/testing.go b/internal/configs/configload/testing.go index bc90b2ef3cbc..537e86ac2c54 100644 --- a/internal/configs/configload/testing.go +++ b/internal/configs/configload/testing.go @@ -4,9 +4,10 @@ package configload import ( - "io/ioutil" "os" "testing" + + "github.com/hashicorp/terraform/internal/configs" ) // NewLoaderForTests is a variant of NewLoader that is intended to be more @@ -20,10 +21,10 @@ import ( // In the case of any errors, t.Fatal (or similar) will be called to halt // execution of the test, so the calling test does not need to handle errors // itself. -func NewLoaderForTests(t testing.TB) (*Loader, func()) { +func NewLoaderForTests(t testing.TB, parserOpts ...configs.Option) (*Loader, func()) { t.Helper() - modulesDir, err := ioutil.TempDir("", "tf-configs") + modulesDir, err := os.MkdirTemp("", "tf-configs") if err != nil { t.Fatalf("failed to create temporary modules dir: %s", err) return nil, func() {} @@ -36,6 +37,7 @@ func NewLoaderForTests(t testing.TB) (*Loader, func()) { loader, err := NewLoader(&Config{ ModulesDir: modulesDir, }) + loader.parserOpts = append(loader.parserOpts, parserOpts...) if err != nil { cleanup() t.Fatalf("failed to create config loader: %s", err) diff --git a/internal/configs/parser_config_dir.go b/internal/configs/parser_config_dir.go index 9c296bc01323..a4c220febc8e 100644 --- a/internal/configs/parser_config_dir.go +++ b/internal/configs/parser_config_dir.go @@ -153,8 +153,8 @@ func (p Parser) ConfigDirFiles(dir string, opts ...Option) (primary, override [] // exists and contains at least one Terraform config file (with a .tf or // .tf.json extension.). Note, we explicitely exclude checking for tests here // as tests must live alongside actual .tf config files. Same goes for query files. -func (p *Parser) IsConfigDir(path string) bool { - pathSet, _ := p.dirFileSet(path) +func (p *Parser) IsConfigDir(path string, opts ...Option) bool { + pathSet, _ := p.dirFileSet(path, opts...) return (len(pathSet.Primary) + len(pathSet.Override)) > 0 } diff --git a/internal/configs/parser_config_dir_test.go b/internal/configs/parser_config_dir_test.go index b1921a794ece..2fe176ca6822 100644 --- a/internal/configs/parser_config_dir_test.go +++ b/internal/configs/parser_config_dir_test.go @@ -167,27 +167,23 @@ func TestParserLoadConfigDirWithQueries(t *testing.T) { diagnostics []string listResources int managedResources int - allowExperiments bool }{ { - name: "simple", - directory: "testdata/query-files/valid/simple", - listResources: 3, - allowExperiments: true, + name: "simple", + directory: "testdata/query-files/valid/simple", + listResources: 3, }, { name: "mixed", directory: "testdata/query-files/valid/mixed", listResources: 3, managedResources: 1, - allowExperiments: true, }, { name: "loading query lists with no-experiments", directory: "testdata/query-files/valid/mixed", managedResources: 1, - listResources: 0, - allowExperiments: false, + listResources: 3, }, { name: "no-provider", @@ -195,7 +191,6 @@ func TestParserLoadConfigDirWithQueries(t *testing.T) { diagnostics: []string{ "testdata/query-files/invalid/no-provider/main.tfquery.hcl:1,1-27: Missing \"provider\" attribute; You must specify a provider attribute when defining a list block.", }, - allowExperiments: true, }, { name: "with-depends-on", @@ -203,16 +198,14 @@ func TestParserLoadConfigDirWithQueries(t *testing.T) { diagnostics: []string{ "testdata/query-files/invalid/with-depends-on/main.tfquery.hcl:23,3-13: Unsupported argument; An argument named \"depends_on\" is not expected here.", }, - listResources: 2, - allowExperiments: true, + listResources: 2, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { parser := NewParser(nil) - parser.AllowLanguageExperiments(test.allowExperiments) - mod, diags := parser.LoadConfigDir(test.directory) + mod, diags := parser.LoadConfigDir(test.directory, MatchQueryFiles()) if len(test.diagnostics) > 0 { if !diags.HasErrors() { t.Errorf("expected errors, but found none") diff --git a/internal/configs/parser_file_matcher.go b/internal/configs/parser_file_matcher.go index cb282a1a7d35..6644b421ee6c 100644 --- a/internal/configs/parser_file_matcher.go +++ b/internal/configs/parser_file_matcher.go @@ -64,9 +64,6 @@ func (p *Parser) dirFileSet(dir string, opts ...Option) (ConfigFileSet, hcl.Diag testDirectory: DefaultTestDirectory, fs: p.fs, } - if p.AllowsLanguageExperiments() { - cfg.matchers = append(cfg.matchers, &queryFiles{}) - } for _, opt := range opts { opt(cfg) } @@ -142,6 +139,13 @@ func MatchTestFiles(dir string) Option { } } +// MatchQueryFiles adds a matcher for Terraform query files (.tfquery.hcl and .tfquery.json) +func MatchQueryFiles() Option { + return func(o *parserConfig) { + o.matchers = append(o.matchers, &queryFiles{}) + } +} + // moduleFiles matches regular Terraform configuration files (.tf and .tf.json) type moduleFiles struct{} diff --git a/internal/initwd/testing.go b/internal/initwd/testing.go index 8c99778e1c6b..4331529131da 100644 --- a/internal/initwd/testing.go +++ b/internal/initwd/testing.go @@ -53,7 +53,7 @@ func LoadConfigForTests(t *testing.T, rootDir string, testsDir string) (*configs t.Fatalf("failed to refresh modules after installation: %s", err) } - config, hclDiags := loader.LoadConfig(rootDir, configs.MatchTestFiles(testsDir)) + config, hclDiags := loader.LoadConfigWithTests(rootDir, testsDir) diags = diags.Append(hclDiags) return config, loader, cleanup, diags } diff --git a/internal/terraform/terraform_test.go b/internal/terraform/terraform_test.go index e6e9dc8d077d..a988d30fc6f4 100644 --- a/internal/terraform/terraform_test.go +++ b/internal/terraform/terraform_test.go @@ -97,6 +97,8 @@ func testModuleInline(t testing.TB, sources map[string]string) *configs.Config { t.Fatal(err) } + var queryOpt configs.Option + for path, configStr := range sources { dir := filepath.Dir(path) if dir != "." { @@ -116,9 +118,18 @@ func testModuleInline(t testing.TB, sources map[string]string) *configs.Config { if err != nil { t.Fatalf("Error creating temporary file for config: %s", err) } + + if strings.HasSuffix(path, "tfquery.hcl") || strings.HasSuffix(path, "tfquery.json") { + queryOpt = configs.MatchQueryFiles() + } + } + + var parserOpts []configs.Option + if queryOpt != nil { + parserOpts = append(parserOpts, queryOpt) } - loader, cleanup := configload.NewLoaderForTests(t) + loader, cleanup := configload.NewLoaderForTests(t, parserOpts...) defer cleanup() // We need to be able to exercise experimental features in our integration tests. @@ -139,7 +150,7 @@ func testModuleInline(t testing.TB, sources map[string]string) *configs.Config { t.Fatalf("failed to refresh modules after installation: %s", err) } - config, diags := loader.LoadConfig(cfgPath, configs.MatchTestFiles("tests")) + config, diags := loader.LoadConfigWithTests(cfgPath, "tests") if diags.HasErrors() { t.Fatal(diags.Error()) } diff --git a/internal/tfdiags/object.go b/internal/tfdiags/object.go index 56da886c67e7..8fb115673e3d 100644 --- a/internal/tfdiags/object.go +++ b/internal/tfdiags/object.go @@ -4,6 +4,7 @@ package tfdiags import ( "fmt" + "slices" "strings" "github.com/zclconf/go-cty/cty" @@ -23,29 +24,39 @@ func ObjectToString(obj cty.Value) string { return "" } - if obj.Type().IsObjectType() { - result := "" - it := obj.ElementIterator() - for it.Next() { - key, val := it.Element() - keyStr := key.AsString() + if !obj.Type().IsObjectType() { + panic("not an object") + } - if result != "" { - result += "," - } + it := obj.ElementIterator() + keys := make([]string, 0, obj.LengthInt()) + objMap := make(map[string]cty.Value) + result := "" + // store the keys for the object, and sort them + // before appending to the result so that the final value is deterministic. + for it.Next() { + key, val := it.Element() + keyStr := key.AsString() + keys = append(keys, keyStr) + objMap[keyStr] = val + } - if val.IsNull() { - result += fmt.Sprintf("%s=", keyStr) - continue - } + slices.Sort(keys) + for _, key := range keys { + val := objMap[key] + if result != "" { + result += "," + } - result += fmt.Sprintf("%s=%s", keyStr, ValueToString(val)) + if val.IsNull() { + result += fmt.Sprintf("%s=", key) + continue } - return result + result += fmt.Sprintf("%s=%s", key, ValueToString(val)) } - panic("not an object") + return result } func ValueToString(val cty.Value) string {