Skip to content

Commit 1cb7d18

Browse files
authored
Merge pull request #37515 from hashicorp/jbardin/generate-config
provider GenerateResourceConfig
2 parents 00da430 + e35b7a7 commit 1cb7d18

File tree

31 files changed

+2143
-1232
lines changed

31 files changed

+2143
-1232
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: NEW FEATURES
2+
body: A new GenerateResourceConfiguration RPC allows providers to create more precise configuration values during import.
3+
time: 2025-08-29T15:19:46.781245-04:00
4+
custom:
5+
Issue: "37515"

docs/plugin-protocol/tfplugin5.proto

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,10 @@ message ServerCapabilities {
281281
// The move_resource_state capability signals that a provider supports the
282282
// MoveResourceState RPC.
283283
bool move_resource_state = 3;
284+
285+
// The generate_resource_config capability signals that a provider supports
286+
// GenerateResourceConfig.
287+
bool generate_resource_config = 4;
284288
}
285289

286290
// ClientCapabilities allows Terraform to publish information regarding
@@ -352,6 +356,7 @@ service Provider {
352356
rpc ImportResourceState(ImportResourceState.Request) returns (ImportResourceState.Response);
353357
rpc MoveResourceState(MoveResourceState.Request) returns (MoveResourceState.Response);
354358
rpc ReadDataSource(ReadDataSource.Request) returns (ReadDataSource.Response);
359+
rpc GenerateResourceConfig(GenerateResourceConfig.Request) returns (GenerateResourceConfig.Response);
355360

356361
//////// Ephemeral Resource Lifecycle
357362
rpc ValidateEphemeralResourceConfig(ValidateEphemeralResourceConfig.Request) returns (ValidateEphemeralResourceConfig.Response);
@@ -686,6 +691,19 @@ message ImportResourceState {
686691
}
687692
}
688693

694+
message GenerateResourceConfig {
695+
message Request {
696+
string type_name = 1;
697+
DynamicValue state = 2;
698+
}
699+
700+
message Response {
701+
// config is the provided state modified such that it represents a valid resource configuration value.
702+
DynamicValue config = 1;
703+
repeated Diagnostic diagnostics = 2;
704+
}
705+
}
706+
689707
message MoveResourceState {
690708
message Request {
691709
// The address of the provider the resource is being moved from.

docs/plugin-protocol/tfplugin6.proto

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,10 @@ message ServerCapabilities {
300300
// The move_resource_state capability signals that a provider supports the
301301
// MoveResourceState RPC.
302302
bool move_resource_state = 3;
303+
304+
// The generate_resource_config capability signals that a provider supports
305+
// GenerateResourceConfig.
306+
bool generate_resource_config = 4;
303307
}
304308

305309
// ClientCapabilities allows Terraform to publish information regarding
@@ -371,6 +375,7 @@ service Provider {
371375
rpc ImportResourceState(ImportResourceState.Request) returns (ImportResourceState.Response);
372376
rpc MoveResourceState(MoveResourceState.Request) returns (MoveResourceState.Response);
373377
rpc ReadDataSource(ReadDataSource.Request) returns (ReadDataSource.Response);
378+
rpc GenerateResourceConfig(GenerateResourceConfig.Request) returns (GenerateResourceConfig.Response);
374379

375380
//////// Ephemeral Resource Lifecycle
376381
rpc ValidateEphemeralResourceConfig(ValidateEphemeralResourceConfig.Request) returns (ValidateEphemeralResourceConfig.Response);
@@ -719,6 +724,19 @@ message ImportResourceState {
719724
}
720725
}
721726

727+
message GenerateResourceConfig {
728+
message Request {
729+
string type_name = 1;
730+
DynamicValue state = 2;
731+
}
732+
733+
message Response {
734+
// config is the provided state modified such that it represents a valid resource configuration value.
735+
DynamicValue config = 1;
736+
repeated Diagnostic diagnostics = 2;
737+
}
738+
}
739+
722740
message MoveResourceState {
723741
message Request {
724742
// The address of the provider the resource is being moved from.

internal/builtin/providers/terraform/provider.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,10 @@ func (p *Provider) ReadDataSource(req providers.ReadDataSourceRequest) providers
149149
return res
150150
}
151151

152+
func (p *Provider) GenerateResourceConfig(providers.GenerateResourceConfigRequest) providers.GenerateResourceConfigResponse {
153+
panic("not implemented")
154+
}
155+
152156
// Stop is called when the provider should halt any in-flight actions.
153157
func (p *Provider) Stop() error {
154158
log.Println("[DEBUG] terraform provider cannot Stop")

internal/command/views/hook_json.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -251,10 +251,8 @@ func (h *jsonHook) PostListQuery(id terraform.HookResourceIdentity, results plan
251251
for idx := 0; iter.Next(); idx++ {
252252
_, value := iter.Element()
253253

254-
generated := results.Generated
255-
if generated != nil {
256-
generated = generated.Results[idx]
257-
}
254+
generated := results.Generated.Imports[idx]
255+
258256
result := json.NewQueryResult(addr, value, generated)
259257

260258
h.view.log.Info(

internal/command/views/json/query.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,10 @@ func NewQueryStart(addr addrs.AbsResourceInstance, input_config cty.Value) Query
4242
}
4343
}
4444

45-
func NewQueryResult(listAddr addrs.AbsResourceInstance, value cty.Value, generated *genconfig.Resource) QueryResult {
46-
var config, importConfig string
47-
if generated != nil {
48-
config = generated.String()
49-
importConfig = string(generated.Import)
50-
}
45+
func NewQueryResult(listAddr addrs.AbsResourceInstance, value cty.Value, generated genconfig.ResourceImport) QueryResult {
46+
config := generated.Resource.String()
47+
importConfig := string(generated.ImportBody)
48+
5149
result := QueryResult{
5250
Address: listAddr.String(),
5351
DisplayName: value.GetAttr("display_name").AsString(),

internal/genconfig/generate_config.go

Lines changed: 96 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -22,43 +22,71 @@ import (
2222
"github.com/hashicorp/terraform/internal/tfdiags"
2323
)
2424

25+
// ImportGroup represents one or more resource and import configuration blocks.
26+
type ImportGroup struct {
27+
Imports []ResourceImport
28+
}
29+
30+
// ResourceImport pairs up the import and associated resource when generating
31+
// configuration, so that query output can be more structured for easier
32+
// consumption.
33+
type ResourceImport struct {
34+
ImportBody []byte
35+
Resource Resource
36+
}
37+
2538
type Resource struct {
39+
Addr addrs.AbsResourceInstance
40+
2641
// HCL Body of the resource, which is the attributes and blocks
2742
// that are part of the resource.
2843
Body []byte
44+
}
2945

30-
// Import is the HCL code for the import block. This is only
31-
// generated for list resource results.
32-
Import []byte
33-
Addr addrs.AbsResourceInstance
34-
Results []*Resource
46+
func (r Resource) String() string {
47+
var buf strings.Builder
48+
buf.WriteString(fmt.Sprintf("resource %q %q {\n", r.Addr.Resource.Resource.Type, r.Addr.Resource.Resource.Name))
49+
buf.Write(r.Body)
50+
buf.WriteString("}")
51+
52+
formatted := hclwrite.Format([]byte(buf.String()))
53+
return string(formatted)
3554
}
3655

37-
func (r *Resource) String() string {
56+
func (i ImportGroup) String() string {
3857
var buf strings.Builder
39-
switch r.Addr.Resource.Resource.Mode {
40-
case addrs.ListResourceMode:
41-
last := len(r.Results) - 1
42-
// sort the results by their keys so the output is consistent
43-
for idx, managed := range r.Results {
44-
if managed.Body != nil {
45-
buf.WriteString(managed.String())
46-
buf.WriteString("\n")
47-
}
48-
if managed.Import != nil {
49-
buf.WriteString(string(managed.Import))
50-
buf.WriteString("\n")
51-
}
52-
if idx != last {
53-
buf.WriteString("\n")
54-
}
55-
}
56-
case addrs.ManagedResourceMode:
57-
buf.WriteString(fmt.Sprintf("resource %q %q {\n", r.Addr.Resource.Resource.Type, r.Addr.Resource.Resource.Name))
58-
buf.Write(r.Body)
59-
buf.WriteString("}")
60-
default:
61-
panic(fmt.Errorf("unsupported resource mode %s", r.Addr.Resource.Resource.Mode))
58+
59+
for _, imp := range i.Imports {
60+
buf.WriteString(imp.Resource.String())
61+
buf.WriteString("\n\n")
62+
buf.WriteString(string(imp.ImportBody))
63+
buf.WriteString("\n\n")
64+
}
65+
66+
// The output better be valid HCL which can be parsed and formatted.
67+
formatted := hclwrite.Format([]byte(buf.String()))
68+
return string(formatted)
69+
}
70+
71+
func (i ImportGroup) ResourcesString() string {
72+
var buf strings.Builder
73+
74+
for _, imp := range i.Imports {
75+
buf.WriteString(imp.Resource.String())
76+
buf.WriteString("\n")
77+
}
78+
79+
// The output better be valid HCL which can be parsed and formatted.
80+
formatted := hclwrite.Format([]byte(buf.String()))
81+
return string(formatted)
82+
}
83+
84+
func (i ImportGroup) ImportsString() string {
85+
var buf strings.Builder
86+
87+
for _, imp := range i.Imports {
88+
buf.WriteString(string(imp.ImportBody))
89+
buf.WriteString("\n")
6290
}
6391

6492
// The output better be valid HCL which can be parsed and formatted.
@@ -75,9 +103,9 @@ func (r *Resource) String() string {
75103
func GenerateResourceContents(addr addrs.AbsResourceInstance,
76104
schema *configschema.Block,
77105
pc addrs.LocalProviderConfig,
78-
stateVal cty.Value,
106+
configVal cty.Value,
79107
forceProviderAddr bool,
80-
) (*Resource, tfdiags.Diagnostics) {
108+
) (Resource, tfdiags.Diagnostics) {
81109
var buf strings.Builder
82110

83111
var diags tfdiags.Diagnostics
@@ -89,49 +117,40 @@ func GenerateResourceContents(addr addrs.AbsResourceInstance,
89117
buf.WriteString(fmt.Sprintf("provider = %s\n", pc.StringCompact()))
90118
}
91119

92-
// This is generating configuration, so the only marks should be coming from
93-
// the schema itself.
94-
stateVal, _ = stateVal.UnmarkDeep()
95-
96-
// filter the state down to a suitable config value
97-
stateVal = extractConfigFromState(schema, stateVal)
98-
99-
if stateVal.RawEquals(cty.NilVal) {
120+
if configVal.RawEquals(cty.NilVal) {
100121
diags = diags.Append(writeConfigAttributes(addr, &buf, schema.Attributes, 2))
101122
diags = diags.Append(writeConfigBlocks(addr, &buf, schema.BlockTypes, 2))
102123
} else {
103-
diags = diags.Append(writeConfigAttributesFromExisting(addr, &buf, stateVal, schema.Attributes, 2))
104-
diags = diags.Append(writeConfigBlocksFromExisting(addr, &buf, stateVal, schema.BlockTypes, 2))
124+
diags = diags.Append(writeConfigAttributesFromExisting(addr, &buf, configVal, schema.Attributes, 2))
125+
diags = diags.Append(writeConfigBlocksFromExisting(addr, &buf, configVal, schema.BlockTypes, 2))
105126
}
106127

107128
// The output better be valid HCL which can be parsed and formatted.
108129
formatted := hclwrite.Format([]byte(buf.String()))
109-
return &Resource{
110-
Body: formatted,
111-
Addr: addr,
112-
}, diags
130+
return Resource{Addr: addr, Body: formatted}, diags
131+
}
132+
133+
// ResourceListElement is a single Resource state and identity pair derived from
134+
// a list resource response.
135+
type ResourceListElement struct {
136+
// Config is the cty value extracted from the resource state which is
137+
// intended to be written into the HCL resource block.
138+
Config cty.Value
139+
140+
Identity cty.Value
113141
}
114142

115143
func GenerateListResourceContents(addr addrs.AbsResourceInstance,
116144
schema *configschema.Block,
117145
idSchema *configschema.Object,
118146
pc addrs.LocalProviderConfig,
119-
stateVal cty.Value,
120-
) (*Resource, tfdiags.Diagnostics) {
147+
resources []ResourceListElement,
148+
) (ImportGroup, tfdiags.Diagnostics) {
149+
121150
var diags tfdiags.Diagnostics
122-
if !stateVal.CanIterateElements() {
123-
diags = diags.Append(
124-
hcl.Diagnostic{
125-
Severity: hcl.DiagError,
126-
Summary: "Invalid resource instance value",
127-
Detail: fmt.Sprintf("Resource instance %s has nil or non-iterable value", addr),
128-
})
129-
return nil, diags
130-
}
151+
ret := ImportGroup{}
131152

132-
ret := make([]*Resource, stateVal.LengthInt())
133-
iter := stateVal.ElementIterator()
134-
for idx := 0; iter.Next(); idx++ {
153+
for idx, res := range resources {
135154
// Generate a unique resource name for each instance in the list.
136155
resAddr := addrs.AbsResourceInstance{
137156
Module: addr.Module,
@@ -144,39 +163,34 @@ func GenerateListResourceContents(addr addrs.AbsResourceInstance,
144163
Key: addr.Resource.Key,
145164
},
146165
}
147-
ls := &Resource{Addr: resAddr}
148-
ret[idx] = ls
149-
150-
_, val := iter.Element()
151-
// we still need to generate the resource block even if the state is not given,
152-
// so that the import block can reference it.
153-
stateVal := cty.NilVal
154-
if val.Type().HasAttribute("state") {
155-
stateVal = val.GetAttr("state")
156-
}
157-
content, gDiags := GenerateResourceContents(resAddr, schema, pc, stateVal, true)
166+
167+
content, gDiags := GenerateResourceContents(resAddr, schema, pc, res.Config, true)
158168
if gDiags.HasErrors() {
159169
diags = diags.Append(gDiags)
160170
continue
161171
}
162-
ls.Body = content.Body
163172

164-
idVal := val.GetAttr("identity")
165-
importContent, gDiags := generateImportBlock(resAddr, idSchema, pc, idVal)
173+
resImport := ResourceImport{
174+
Resource: Resource{
175+
Addr: resAddr,
176+
Body: content.Body,
177+
},
178+
}
179+
180+
importContent, gDiags := GenerateImportBlock(resAddr, idSchema, pc, res.Identity)
166181
if gDiags.HasErrors() {
167182
diags = diags.Append(gDiags)
168183
continue
169184
}
170-
ls.Import = bytes.TrimSpace(hclwrite.Format([]byte(importContent)))
185+
186+
resImport.ImportBody = bytes.TrimSpace(hclwrite.Format(importContent.ImportBody))
187+
ret.Imports = append(ret.Imports, resImport)
171188
}
172189

173-
return &Resource{
174-
Results: ret,
175-
Addr: addr,
176-
}, diags
190+
return ret, diags
177191
}
178192

179-
func generateImportBlock(addr addrs.AbsResourceInstance, idSchema *configschema.Object, pc addrs.LocalProviderConfig, identity cty.Value) (string, tfdiags.Diagnostics) {
193+
func GenerateImportBlock(addr addrs.AbsResourceInstance, idSchema *configschema.Object, pc addrs.LocalProviderConfig, identity cty.Value) (ResourceImport, tfdiags.Diagnostics) {
180194
var buf strings.Builder
181195
var diags tfdiags.Diagnostics
182196

@@ -190,7 +204,7 @@ func generateImportBlock(addr addrs.AbsResourceInstance, idSchema *configschema.
190204
buf.WriteString("}\n}\n")
191205

192206
formatted := hclwrite.Format([]byte(buf.String()))
193-
return string(formatted), diags
207+
return ResourceImport{ImportBody: formatted}, diags
194208
}
195209

196210
func writeConfigAttributes(addr addrs.AbsResourceInstance, buf *strings.Builder, attrs map[string]*configschema.Attribute, indent int) tfdiags.Diagnostics {
@@ -638,11 +652,11 @@ func hclEscapeString(str string) string {
638652
return str
639653
}
640654

641-
// extractConfigFromState takes the state value of a resource, and filters the
655+
// ExtractLegacyConfigFromState takes the state value of a resource, and filters the
642656
// value down to what would be acceptable as a resource configuration value.
643657
// This is used when the provider does not implement GenerateResourceConfig to
644658
// create a suitable value.
645-
func extractConfigFromState(schema *configschema.Block, state cty.Value) cty.Value {
659+
func ExtractLegacyConfigFromState(schema *configschema.Block, state cty.Value) cty.Value {
646660
config, _ := cty.Transform(state, func(path cty.Path, v cty.Value) (cty.Value, error) {
647661
if v.IsNull() {
648662
return v, nil

0 commit comments

Comments
 (0)