Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changes/v1.14/NEW FEATURES-20250829-183404.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: NEW FEATURES
body: "**List Resources**: List resources can be defined in `*.tfquery.hcl` files and allow querying and filterting existing infrastructure."
time: 2025-08-29T18:34:04.250038+02:00
3 changes: 3 additions & 0 deletions .changes/v1.14/NEW FEATURES-20250829-184206.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: NEW FEATURES
body: "A new Terraform command `terraform query`: Executes list operations against existing infrastructure and displays the results. The command can optionally generate configuration for importing results into Terraform."
time: 2025-08-29T18:42:06.659172+02:00
12 changes: 6 additions & 6 deletions commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 4 additions & 3 deletions internal/cloud/backend_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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 {
Expand All @@ -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 = `
Expand Down
12 changes: 10 additions & 2 deletions internal/cloud/backend_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
30 changes: 30 additions & 0 deletions internal/command/e2etest/providers_schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand Down
6 changes: 3 additions & 3 deletions internal/command/jsonformat/plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion internal/command/jsonformat/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
32 changes: 15 additions & 17 deletions internal/command/jsonprovider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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
}
67 changes: 14 additions & 53 deletions internal/command/jsonprovider/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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},
},
},
},
},
}
}
2 changes: 1 addition & 1 deletion internal/command/providers_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion internal/command/state_show.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
2 changes: 1 addition & 1 deletion internal/command/views/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
4 changes: 2 additions & 2 deletions internal/command/views/show.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down Expand Up @@ -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)
Expand Down
Loading