tf query: ready list blocks for beta (#37619)

This commit is contained in:
Samsondeen 2025-09-17 12:29:04 +02:00 committed by GitHub
parent 5981cd2bf7
commit 719aefdb89
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 247 additions and 168 deletions

View file

@ -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,

View file

@ -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 = `

View file

@ -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)

View file

@ -1 +1,3 @@
list "concept_pet" "pets" {}
list "concept_pet" "pets" {
provider = concept
}

View file

@ -1 +1,3 @@
list "null_resource" "foo" {}
list "null_resource" "foo" {
provider = null
}

View file

@ -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",

View file

@ -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,

View file

@ -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()

View file

@ -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
}

View file

@ -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},
},
},
},
},
}
}

View file

@ -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 {

View file

@ -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

View file

@ -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".

View file

@ -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

View file

@ -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)

View file

@ -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 {

View file

@ -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{

View file

@ -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,
}

View file

@ -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)

View file

@ -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,
}

View file

@ -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
}),

View file

@ -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

View file

@ -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) {

View file

@ -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
}

View file

@ -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)

View file

@ -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
}

View file

@ -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")

View file

@ -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{}

View file

@ -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
}

View file

@ -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()
}
}
loader, cleanup := configload.NewLoaderForTests(t)
var parserOpts []configs.Option
if queryOpt != nil {
parserOpts = append(parserOpts, queryOpt)
}
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())
}

View file

@ -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 "<empty>"
}
if obj.Type().IsObjectType() {
result := ""
it := obj.ElementIterator()
for it.Next() {
key, val := it.Element()
keyStr := key.AsString()
if result != "" {
result += ","
}
if val.IsNull() {
result += fmt.Sprintf("%s=<null>", keyStr)
continue
}
result += fmt.Sprintf("%s=%s", keyStr, ValueToString(val))
}
return result
if !obj.Type().IsObjectType() {
panic("not an object")
}
panic("not an object")
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
}
slices.Sort(keys)
for _, key := range keys {
val := objMap[key]
if result != "" {
result += ","
}
if val.IsNull() {
result += fmt.Sprintf("%s=<null>", key)
continue
}
result += fmt.Sprintf("%s=%s", key, ValueToString(val))
}
return result
}
func ValueToString(val cty.Value) string {