Merge pull request #37515 from hashicorp/jbardin/generate-config

provider GenerateResourceConfig
pull/37567/head
James Bardin 6 months ago committed by GitHub
commit 1cb7d1859f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
kind: NEW FEATURES
body: A new GenerateResourceConfiguration RPC allows providers to create more precise configuration values during import.
time: 2025-08-29T15:19:46.781245-04:00
custom:
Issue: "37515"

@ -281,6 +281,10 @@ message ServerCapabilities {
// The move_resource_state capability signals that a provider supports the
// MoveResourceState RPC.
bool move_resource_state = 3;
// The generate_resource_config capability signals that a provider supports
// GenerateResourceConfig.
bool generate_resource_config = 4;
}
// ClientCapabilities allows Terraform to publish information regarding
@ -352,6 +356,7 @@ service Provider {
rpc ImportResourceState(ImportResourceState.Request) returns (ImportResourceState.Response);
rpc MoveResourceState(MoveResourceState.Request) returns (MoveResourceState.Response);
rpc ReadDataSource(ReadDataSource.Request) returns (ReadDataSource.Response);
rpc GenerateResourceConfig(GenerateResourceConfig.Request) returns (GenerateResourceConfig.Response);
//////// Ephemeral Resource Lifecycle
rpc ValidateEphemeralResourceConfig(ValidateEphemeralResourceConfig.Request) returns (ValidateEphemeralResourceConfig.Response);
@ -686,6 +691,19 @@ message ImportResourceState {
}
}
message GenerateResourceConfig {
message Request {
string type_name = 1;
DynamicValue state = 2;
}
message Response {
// config is the provided state modified such that it represents a valid resource configuration value.
DynamicValue config = 1;
repeated Diagnostic diagnostics = 2;
}
}
message MoveResourceState {
message Request {
// The address of the provider the resource is being moved from.

@ -300,6 +300,10 @@ message ServerCapabilities {
// The move_resource_state capability signals that a provider supports the
// MoveResourceState RPC.
bool move_resource_state = 3;
// The generate_resource_config capability signals that a provider supports
// GenerateResourceConfig.
bool generate_resource_config = 4;
}
// ClientCapabilities allows Terraform to publish information regarding
@ -371,6 +375,7 @@ service Provider {
rpc ImportResourceState(ImportResourceState.Request) returns (ImportResourceState.Response);
rpc MoveResourceState(MoveResourceState.Request) returns (MoveResourceState.Response);
rpc ReadDataSource(ReadDataSource.Request) returns (ReadDataSource.Response);
rpc GenerateResourceConfig(GenerateResourceConfig.Request) returns (GenerateResourceConfig.Response);
//////// Ephemeral Resource Lifecycle
rpc ValidateEphemeralResourceConfig(ValidateEphemeralResourceConfig.Request) returns (ValidateEphemeralResourceConfig.Response);
@ -719,6 +724,19 @@ message ImportResourceState {
}
}
message GenerateResourceConfig {
message Request {
string type_name = 1;
DynamicValue state = 2;
}
message Response {
// config is the provided state modified such that it represents a valid resource configuration value.
DynamicValue config = 1;
repeated Diagnostic diagnostics = 2;
}
}
message MoveResourceState {
message Request {
// The address of the provider the resource is being moved from.

@ -149,6 +149,10 @@ func (p *Provider) ReadDataSource(req providers.ReadDataSourceRequest) providers
return res
}
func (p *Provider) GenerateResourceConfig(providers.GenerateResourceConfigRequest) providers.GenerateResourceConfigResponse {
panic("not implemented")
}
// Stop is called when the provider should halt any in-flight actions.
func (p *Provider) Stop() error {
log.Println("[DEBUG] terraform provider cannot Stop")

@ -251,10 +251,8 @@ func (h *jsonHook) PostListQuery(id terraform.HookResourceIdentity, results plan
for idx := 0; iter.Next(); idx++ {
_, value := iter.Element()
generated := results.Generated
if generated != nil {
generated = generated.Results[idx]
}
generated := results.Generated.Imports[idx]
result := json.NewQueryResult(addr, value, generated)
h.view.log.Info(

@ -42,12 +42,10 @@ func NewQueryStart(addr addrs.AbsResourceInstance, input_config cty.Value) Query
}
}
func NewQueryResult(listAddr addrs.AbsResourceInstance, value cty.Value, generated *genconfig.Resource) QueryResult {
var config, importConfig string
if generated != nil {
config = generated.String()
importConfig = string(generated.Import)
}
func NewQueryResult(listAddr addrs.AbsResourceInstance, value cty.Value, generated genconfig.ResourceImport) QueryResult {
config := generated.Resource.String()
importConfig := string(generated.ImportBody)
result := QueryResult{
Address: listAddr.String(),
DisplayName: value.GetAttr("display_name").AsString(),

@ -22,43 +22,71 @@ import (
"github.com/hashicorp/terraform/internal/tfdiags"
)
// ImportGroup represents one or more resource and import configuration blocks.
type ImportGroup struct {
Imports []ResourceImport
}
// ResourceImport pairs up the import and associated resource when generating
// configuration, so that query output can be more structured for easier
// consumption.
type ResourceImport struct {
ImportBody []byte
Resource Resource
}
type Resource struct {
Addr addrs.AbsResourceInstance
// HCL Body of the resource, which is the attributes and blocks
// that are part of the resource.
Body []byte
}
// Import is the HCL code for the import block. This is only
// generated for list resource results.
Import []byte
Addr addrs.AbsResourceInstance
Results []*Resource
func (r Resource) String() string {
var buf strings.Builder
buf.WriteString(fmt.Sprintf("resource %q %q {\n", r.Addr.Resource.Resource.Type, r.Addr.Resource.Resource.Name))
buf.Write(r.Body)
buf.WriteString("}")
formatted := hclwrite.Format([]byte(buf.String()))
return string(formatted)
}
func (r *Resource) String() string {
func (i ImportGroup) String() string {
var buf strings.Builder
switch r.Addr.Resource.Resource.Mode {
case addrs.ListResourceMode:
last := len(r.Results) - 1
// sort the results by their keys so the output is consistent
for idx, managed := range r.Results {
if managed.Body != nil {
buf.WriteString(managed.String())
buf.WriteString("\n")
}
if managed.Import != nil {
buf.WriteString(string(managed.Import))
buf.WriteString("\n")
}
if idx != last {
buf.WriteString("\n")
}
}
case addrs.ManagedResourceMode:
buf.WriteString(fmt.Sprintf("resource %q %q {\n", r.Addr.Resource.Resource.Type, r.Addr.Resource.Resource.Name))
buf.Write(r.Body)
buf.WriteString("}")
default:
panic(fmt.Errorf("unsupported resource mode %s", r.Addr.Resource.Resource.Mode))
for _, imp := range i.Imports {
buf.WriteString(imp.Resource.String())
buf.WriteString("\n\n")
buf.WriteString(string(imp.ImportBody))
buf.WriteString("\n\n")
}
// The output better be valid HCL which can be parsed and formatted.
formatted := hclwrite.Format([]byte(buf.String()))
return string(formatted)
}
func (i ImportGroup) ResourcesString() string {
var buf strings.Builder
for _, imp := range i.Imports {
buf.WriteString(imp.Resource.String())
buf.WriteString("\n")
}
// The output better be valid HCL which can be parsed and formatted.
formatted := hclwrite.Format([]byte(buf.String()))
return string(formatted)
}
func (i ImportGroup) ImportsString() string {
var buf strings.Builder
for _, imp := range i.Imports {
buf.WriteString(string(imp.ImportBody))
buf.WriteString("\n")
}
// The output better be valid HCL which can be parsed and formatted.
@ -75,9 +103,9 @@ func (r *Resource) String() string {
func GenerateResourceContents(addr addrs.AbsResourceInstance,
schema *configschema.Block,
pc addrs.LocalProviderConfig,
stateVal cty.Value,
configVal cty.Value,
forceProviderAddr bool,
) (*Resource, tfdiags.Diagnostics) {
) (Resource, tfdiags.Diagnostics) {
var buf strings.Builder
var diags tfdiags.Diagnostics
@ -89,49 +117,40 @@ func GenerateResourceContents(addr addrs.AbsResourceInstance,
buf.WriteString(fmt.Sprintf("provider = %s\n", pc.StringCompact()))
}
// This is generating configuration, so the only marks should be coming from
// the schema itself.
stateVal, _ = stateVal.UnmarkDeep()
// filter the state down to a suitable config value
stateVal = extractConfigFromState(schema, stateVal)
if stateVal.RawEquals(cty.NilVal) {
if configVal.RawEquals(cty.NilVal) {
diags = diags.Append(writeConfigAttributes(addr, &buf, schema.Attributes, 2))
diags = diags.Append(writeConfigBlocks(addr, &buf, schema.BlockTypes, 2))
} else {
diags = diags.Append(writeConfigAttributesFromExisting(addr, &buf, stateVal, schema.Attributes, 2))
diags = diags.Append(writeConfigBlocksFromExisting(addr, &buf, stateVal, schema.BlockTypes, 2))
diags = diags.Append(writeConfigAttributesFromExisting(addr, &buf, configVal, schema.Attributes, 2))
diags = diags.Append(writeConfigBlocksFromExisting(addr, &buf, configVal, schema.BlockTypes, 2))
}
// The output better be valid HCL which can be parsed and formatted.
formatted := hclwrite.Format([]byte(buf.String()))
return &Resource{
Body: formatted,
Addr: addr,
}, diags
return Resource{Addr: addr, Body: formatted}, diags
}
// ResourceListElement is a single Resource state and identity pair derived from
// a list resource response.
type ResourceListElement struct {
// Config is the cty value extracted from the resource state which is
// intended to be written into the HCL resource block.
Config cty.Value
Identity cty.Value
}
func GenerateListResourceContents(addr addrs.AbsResourceInstance,
schema *configschema.Block,
idSchema *configschema.Object,
pc addrs.LocalProviderConfig,
stateVal cty.Value,
) (*Resource, tfdiags.Diagnostics) {
resources []ResourceListElement,
) (ImportGroup, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
if !stateVal.CanIterateElements() {
diags = diags.Append(
hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid resource instance value",
Detail: fmt.Sprintf("Resource instance %s has nil or non-iterable value", addr),
})
return nil, diags
}
ret := ImportGroup{}
ret := make([]*Resource, stateVal.LengthInt())
iter := stateVal.ElementIterator()
for idx := 0; iter.Next(); idx++ {
for idx, res := range resources {
// Generate a unique resource name for each instance in the list.
resAddr := addrs.AbsResourceInstance{
Module: addr.Module,
@ -144,39 +163,34 @@ func GenerateListResourceContents(addr addrs.AbsResourceInstance,
Key: addr.Resource.Key,
},
}
ls := &Resource{Addr: resAddr}
ret[idx] = ls
_, val := iter.Element()
// we still need to generate the resource block even if the state is not given,
// so that the import block can reference it.
stateVal := cty.NilVal
if val.Type().HasAttribute("state") {
stateVal = val.GetAttr("state")
}
content, gDiags := GenerateResourceContents(resAddr, schema, pc, stateVal, true)
content, gDiags := GenerateResourceContents(resAddr, schema, pc, res.Config, true)
if gDiags.HasErrors() {
diags = diags.Append(gDiags)
continue
}
ls.Body = content.Body
idVal := val.GetAttr("identity")
importContent, gDiags := generateImportBlock(resAddr, idSchema, pc, idVal)
resImport := ResourceImport{
Resource: Resource{
Addr: resAddr,
Body: content.Body,
},
}
importContent, gDiags := GenerateImportBlock(resAddr, idSchema, pc, res.Identity)
if gDiags.HasErrors() {
diags = diags.Append(gDiags)
continue
}
ls.Import = bytes.TrimSpace(hclwrite.Format([]byte(importContent)))
resImport.ImportBody = bytes.TrimSpace(hclwrite.Format(importContent.ImportBody))
ret.Imports = append(ret.Imports, resImport)
}
return &Resource{
Results: ret,
Addr: addr,
}, diags
return ret, diags
}
func generateImportBlock(addr addrs.AbsResourceInstance, idSchema *configschema.Object, pc addrs.LocalProviderConfig, identity cty.Value) (string, tfdiags.Diagnostics) {
func GenerateImportBlock(addr addrs.AbsResourceInstance, idSchema *configschema.Object, pc addrs.LocalProviderConfig, identity cty.Value) (ResourceImport, tfdiags.Diagnostics) {
var buf strings.Builder
var diags tfdiags.Diagnostics
@ -190,7 +204,7 @@ func generateImportBlock(addr addrs.AbsResourceInstance, idSchema *configschema.
buf.WriteString("}\n}\n")
formatted := hclwrite.Format([]byte(buf.String()))
return string(formatted), diags
return ResourceImport{ImportBody: formatted}, diags
}
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 {
return str
}
// extractConfigFromState takes the state value of a resource, and filters the
// ExtractLegacyConfigFromState takes the state value of a resource, and filters the
// value down to what would be acceptable as a resource configuration value.
// This is used when the provider does not implement GenerateResourceConfig to
// create a suitable value.
func extractConfigFromState(schema *configschema.Block, state cty.Value) cty.Value {
func ExtractLegacyConfigFromState(schema *configschema.Block, state cty.Value) cty.Value {
config, _ := cty.Transform(state, func(path cty.Path, v cty.Value) (cty.Value, error) {
if v.IsNull() {
return v, nil

@ -829,7 +829,11 @@ resource "tfcoremock_sensitive_values" "values" {
if err != nil {
t.Fatalf("schema failed InternalValidate: %s", err)
}
contents, diags := GenerateResourceContents(tc.addr, tc.schema, tc.provider, tc.value, false)
val, _ := tc.value.UnmarkDeep()
config := ExtractLegacyConfigFromState(tc.schema, val)
contents, diags := GenerateResourceContents(tc.addr, tc.schema, tc.provider, config, false)
if len(diags) > 0 {
t.Errorf("expected no diagnostics but found %s", diags)
}
@ -957,8 +961,29 @@ func TestGenerateResourceAndIDContents(t *testing.T) {
LocalName: "aws",
}
// the handling of the list value was moved to the caller, so break it back down in the same way here
var listElements []ResourceListElement
iter := value.ElementIterator()
for iter.Next() {
_, val := iter.Element()
// we still need to generate the resource block even if the state is not given,
// so that the import block can reference it.
stateVal := cty.NilVal
if val.Type().HasAttribute("state") {
stateVal = val.GetAttr("state")
}
stateVal, _ = stateVal.UnmarkDeep()
config := ExtractLegacyConfigFromState(schema, stateVal)
idVal := val.GetAttr("identity")
listElements = append(listElements, ResourceListElement{Config: config, Identity: idVal})
}
// Generate content
content, diags := GenerateListResourceContents(instAddr1, schema, idSchema, pc, value)
content, diags := GenerateListResourceContents(instAddr1, schema, idSchema, pc, listElements)
// Check for diagnostics
if diags.HasErrors() {
t.Fatalf("unexpected diagnostics: %s", diags.Err())
@ -977,6 +1002,7 @@ func TestGenerateResourceAndIDContents(t *testing.T) {
subnet_id = "subnet-123"
}
}
import {
to = aws_instance.example_0
provider = aws
@ -997,6 +1023,7 @@ resource "aws_instance" "example_1" {
subnet_id = "subnet-456"
}
}
import {
to = aws_instance.example_1
provider = aws
@ -1004,6 +1031,7 @@ import {
id = "i-123456"
}
}
`
// Normalize both strings by removing extra whitespace for comparison
normalizeString := func(s string) string {
@ -1018,9 +1046,9 @@ import {
normalizedExpected := normalizeString(expectedContent)
var merged string
res := content.Results
for _, addr := range res {
merged += addr.String()
for _, imp := range content.Imports {
merged += imp.Resource.String()
}
normalizedActual := normalizeString(content.String())

@ -112,6 +112,7 @@ func (p *provider) GetSchema(_ context.Context, req *tfplugin5.GetProviderSchema
GetProviderSchemaOptional: p.schema.ServerCapabilities.GetProviderSchemaOptional,
PlanDestroy: p.schema.ServerCapabilities.PlanDestroy,
MoveResourceState: p.schema.ServerCapabilities.MoveResourceState,
GenerateResourceConfig: p.schema.ServerCapabilities.GenerateResourceConfig,
}
// include any diagnostics from the original GetSchema call
@ -563,6 +564,10 @@ func (p *provider) ImportResourceState(_ context.Context, req *tfplugin5.ImportR
return resp, nil
}
func (p *provider) GenerateResourceConfig(context.Context, *tfplugin5.GenerateResourceConfig_Request) (*tfplugin5.GenerateResourceConfig_Response, error) {
panic("not implemented")
}
func (p *provider) MoveResourceState(_ context.Context, request *tfplugin5.MoveResourceState_Request) (*tfplugin5.MoveResourceState_Response, error) {
resp := &tfplugin5.MoveResourceState_Response{}

@ -121,6 +121,7 @@ func (p *provider6) GetProviderSchema(_ context.Context, req *tfplugin6.GetProvi
GetProviderSchemaOptional: p.schema.ServerCapabilities.GetProviderSchemaOptional,
PlanDestroy: p.schema.ServerCapabilities.PlanDestroy,
MoveResourceState: p.schema.ServerCapabilities.MoveResourceState,
GenerateResourceConfig: p.schema.ServerCapabilities.GenerateResourceConfig,
}
// include any diagnostics from the original GetSchema call
@ -567,6 +568,10 @@ func (p *provider6) ImportResourceState(_ context.Context, req *tfplugin6.Import
return resp, nil
}
func (p *provider6) GenerateResourceConfig(context.Context, *tfplugin6.GenerateResourceConfig_Request) (*tfplugin6.GenerateResourceConfig_Response, error) {
panic("not implemented")
}
func (p *provider6) MoveResourceState(_ context.Context, request *tfplugin6.MoveResourceState_Request) (*tfplugin6.MoveResourceState_Response, error) {
resp := &tfplugin6.MoveResourceState_Response{}

@ -296,7 +296,7 @@ type QueryInstance struct {
type QueryResults struct {
Value cty.Value
Generated *genconfig.Resource
Generated genconfig.ImportGroup
}
func (qi *QueryInstance) DeepCopy() *QueryInstance {

@ -207,7 +207,7 @@ type QueryInstanceSrc struct {
Addr addrs.AbsResourceInstance
ProviderAddr addrs.AbsProviderConfig
Results DynamicValue
Generated *genconfig.Resource
Generated genconfig.ImportGroup
}
func (qis *QueryInstanceSrc) Decode(schema providers.Schema) (*QueryInstance, error) {

@ -206,6 +206,7 @@ func (p *GRPCProvider) GetProviderSchema() providers.GetProviderSchemaResponse {
resp.ServerCapabilities.PlanDestroy = protoResp.ServerCapabilities.PlanDestroy
resp.ServerCapabilities.GetProviderSchemaOptional = protoResp.ServerCapabilities.GetProviderSchemaOptional
resp.ServerCapabilities.MoveResourceState = protoResp.ServerCapabilities.MoveResourceState
resp.ServerCapabilities.GenerateResourceConfig = protoResp.ServerCapabilities.GenerateResourceConfig
}
// set the global cache if we can
@ -940,6 +941,45 @@ func (p *GRPCProvider) ImportResourceState(r providers.ImportResourceStateReques
return resp
}
func (p *GRPCProvider) GenerateResourceConfig(r providers.GenerateResourceConfigRequest) (resp providers.GenerateResourceConfigResponse) {
logger.Trace("GRPCProvider: GenerateResourceConfig")
schema := p.GetProviderSchema()
if schema.Diagnostics.HasErrors() {
resp.Diagnostics = schema.Diagnostics
return resp
}
resSchema, ok := schema.ResourceTypes[r.TypeName]
if !ok {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown resource type %q", r.TypeName))
return resp
}
protoReq := &proto.GenerateResourceConfig_Request{
TypeName: r.TypeName,
State: nil,
}
protoResp, err := p.client.GenerateResourceConfig(p.ctx, protoReq)
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
return resp
}
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics))
ty := resSchema.Body.ImpliedType()
state, err := decodeDynamicValue(protoResp.Config, ty)
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
return resp
}
resp.Config = state
return resp
}
func (p *GRPCProvider) MoveResourceState(r providers.MoveResourceStateRequest) (resp providers.MoveResourceStateResponse) {
logger.Trace("GRPCProvider: MoveResourceState")

@ -122,6 +122,26 @@ func (mr *MockProviderClientMockRecorder) Configure(arg0, arg1 any, arg2 ...any)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Configure", reflect.TypeOf((*MockProviderClient)(nil).Configure), varargs...)
}
// GenerateResourceConfig mocks base method.
func (m *MockProviderClient) GenerateResourceConfig(arg0 context.Context, arg1 *tfplugin5.GenerateResourceConfig_Request, arg2 ...grpc.CallOption) (*tfplugin5.GenerateResourceConfig_Response, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "GenerateResourceConfig", varargs...)
ret0, _ := ret[0].(*tfplugin5.GenerateResourceConfig_Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GenerateResourceConfig indicates an expected call of GenerateResourceConfig.
func (mr *MockProviderClientMockRecorder) GenerateResourceConfig(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateResourceConfig", reflect.TypeOf((*MockProviderClient)(nil).GenerateResourceConfig), varargs...)
}
// GetFunctions mocks base method.
func (m *MockProviderClient) GetFunctions(arg0 context.Context, arg1 *tfplugin5.GetFunctions_Request, arg2 ...grpc.CallOption) (*tfplugin5.GetFunctions_Response, error) {
m.ctrl.T.Helper()

@ -211,6 +211,7 @@ func (p *GRPCProvider) GetProviderSchema() providers.GetProviderSchemaResponse {
resp.ServerCapabilities.PlanDestroy = protoResp.ServerCapabilities.PlanDestroy
resp.ServerCapabilities.GetProviderSchemaOptional = protoResp.ServerCapabilities.GetProviderSchemaOptional
resp.ServerCapabilities.MoveResourceState = protoResp.ServerCapabilities.MoveResourceState
resp.ServerCapabilities.GenerateResourceConfig = protoResp.ServerCapabilities.GenerateResourceConfig
}
// set the global cache if we can
@ -933,8 +934,47 @@ func (p *GRPCProvider) ImportResourceState(r providers.ImportResourceStateReques
return resp
}
func (p *GRPCProvider) GenerateResourceConfig(r providers.GenerateResourceConfigRequest) (resp providers.GenerateResourceConfigResponse) {
logger.Trace("GRPCProvider.v6: GenerateResourceConfig")
schema := p.GetProviderSchema()
if schema.Diagnostics.HasErrors() {
resp.Diagnostics = schema.Diagnostics
return resp
}
resSchema, ok := schema.ResourceTypes[r.TypeName]
if !ok {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown resource type %q", r.TypeName))
return resp
}
protoReq := &proto6.GenerateResourceConfig_Request{
TypeName: r.TypeName,
State: nil,
}
protoResp, err := p.client.GenerateResourceConfig(p.ctx, protoReq)
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
return resp
}
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics))
ty := resSchema.Body.ImpliedType()
state, err := decodeDynamicValue(protoResp.Config, ty)
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
return resp
}
resp.Config = state
return resp
}
func (p *GRPCProvider) MoveResourceState(r providers.MoveResourceStateRequest) (resp providers.MoveResourceStateResponse) {
logger.Trace("GRPCProvider: MoveResourceState")
logger.Trace("GRPCProvider.v6: MoveResourceState")
var sourceIdentity *proto6.RawState
if len(r.SourceIdentity) > 0 {

@ -162,6 +162,26 @@ func (mr *MockProviderClientMockRecorder) DeleteState(arg0, arg1 any, arg2 ...an
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteState", reflect.TypeOf((*MockProviderClient)(nil).DeleteState), varargs...)
}
// GenerateResourceConfig mocks base method.
func (m *MockProviderClient) GenerateResourceConfig(arg0 context.Context, arg1 *tfplugin6.GenerateResourceConfig_Request, arg2 ...grpc.CallOption) (*tfplugin6.GenerateResourceConfig_Response, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "GenerateResourceConfig", varargs...)
ret0, _ := ret[0].(*tfplugin6.GenerateResourceConfig_Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GenerateResourceConfig indicates an expected call of GenerateResourceConfig.
func (mr *MockProviderClientMockRecorder) GenerateResourceConfig(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateResourceConfig", reflect.TypeOf((*MockProviderClient)(nil).GenerateResourceConfig), varargs...)
}
// GetFunctions mocks base method.
func (m *MockProviderClient) GetFunctions(arg0 context.Context, arg1 *tfplugin6.GetFunctions_Request, arg2 ...grpc.CallOption) (*tfplugin6.GetFunctions_Response, error) {
m.ctrl.T.Helper()

@ -169,6 +169,10 @@ func (s simple) ReadResource(req providers.ReadResourceRequest) (resp providers.
return resp
}
func (s simple) GenerateResourceConfig(req providers.GenerateResourceConfigRequest) (resp providers.GenerateResourceConfigResponse) {
panic("not implemented")
}
func (s simple) PlanResourceChange(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
if req.ProposedNewState.IsNull() {
// destroy op

@ -149,6 +149,10 @@ func (s simple) ReadResource(req providers.ReadResourceRequest) (resp providers.
return resp
}
func (s simple) GenerateResourceConfig(req providers.GenerateResourceConfigRequest) (resp providers.GenerateResourceConfigResponse) {
panic("not implemented")
}
func (s simple) PlanResourceChange(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
if req.ProposedNewState.IsNull() {
// destroy op

@ -323,6 +323,10 @@ func (m *Mock) ImportResourceState(request ImportResourceStateRequest) (response
return response
}
func (m *Mock) GenerateResourceConfig(request GenerateResourceConfigRequest) (response GenerateResourceConfigResponse) {
panic("not implemented")
}
func (m *Mock) MoveResourceState(request MoveResourceStateRequest) MoveResourceStateResponse {
// The MoveResourceState operation happens offline, so we can just hand this
// off to the underlying provider.

@ -87,6 +87,11 @@ type Interface interface {
// ImportResourceState requests that the given resource be imported.
ImportResourceState(ImportResourceStateRequest) ImportResourceStateResponse
// GenerateResourceConfig sends a resource state to the provider, and
// expects the provider to return an object which represents a valid
// configuration.
GenerateResourceConfig(GenerateResourceConfigRequest) GenerateResourceConfigResponse
// MoveResourceState retrieves the updated value for a resource after it
// has moved resource types.
MoveResourceState(MoveResourceStateRequest) MoveResourceStateResponse
@ -269,6 +274,11 @@ type ServerCapabilities struct {
// The MoveResourceState capability indicates that this provider supports
// the MoveResourceState RPC.
MoveResourceState bool
// GenerateResourceConfig indicates that the provider can take an existing
// state for a resource instance, and return the subset of the state which
// can be used as configuration.
GenerateResourceConfig bool
}
// ClientCapabilities allows Terraform to publish information regarding
@ -661,6 +671,20 @@ type ImportResourceStateResponse struct {
Deferred *Deferred
}
// GenerateResourceConfigRequest contains the most recent state of a resource
// instance which the provider can use to generate a valid configuration object.
type GenerateResourceConfigRequest struct {
TypeName string
State cty.Value
}
type GenerateResourceConfigResponse struct {
// Config is the subset of the resource state which represents a valid
// configuration object for the instance.
Config cty.Value
Diagnostics tfdiags.Diagnostics
}
// ImportedResource represents an object being imported into Terraform with the
// help of a provider. An ImportedResource is a RemoteObject that has been read
// by the provider's import handler but hasn't yet been committed to state.

@ -94,6 +94,11 @@ type MockProvider struct {
ImportResourceStateRequest providers.ImportResourceStateRequest
ImportResourceStateFn func(providers.ImportResourceStateRequest) providers.ImportResourceStateResponse
GenerateResourceConfigCalled bool
GenerateResourceConfigResponse *providers.GenerateResourceConfigResponse
GenerateResourceConfigRequest providers.GenerateResourceConfigRequest
GenerateResourceConfigFn func(providers.GenerateResourceConfigRequest) providers.GenerateResourceConfigResponse
MoveResourceStateCalled bool
MoveResourceStateResponse *providers.MoveResourceStateResponse
MoveResourceStateRequest providers.MoveResourceStateRequest
@ -744,6 +749,20 @@ func (p *MockProvider) ImportResourceState(r providers.ImportResourceStateReques
return resp
}
func (p *MockProvider) GenerateResourceConfig(r providers.GenerateResourceConfigRequest) (resp providers.GenerateResourceConfigResponse) {
defer p.beginWrite()()
if p.GenerateResourceConfigResponse != nil {
return *p.GenerateResourceConfigResponse
}
if p.GenerateResourceConfigFn != nil {
return p.GenerateResourceConfigFn(r)
}
panic("GenerateResourceConfigFn or GenerateResourceConfigResponse required")
}
func (p *MockProvider) MoveResourceState(r providers.MoveResourceStateRequest) (resp providers.MoveResourceStateResponse) {
defer p.beginWrite()()

@ -81,6 +81,10 @@ func (provider *mockProvider) ImportResourceState(providers.ImportResourceStateR
panic("not implemented in mock")
}
func (p *mockProvider) GenerateResourceConfig(r providers.GenerateResourceConfigRequest) (resp providers.GenerateResourceConfigResponse) {
panic("not implemented in mock")
}
func (provider *mockProvider) MoveResourceState(providers.MoveResourceStateRequest) providers.MoveResourceStateResponse {
if provider.moveResourceError != nil {
return providers.MoveResourceStateResponse{

@ -132,6 +132,11 @@ func (p *erroredProvider) ReadResource(req providers.ReadResourceRequest) provid
}
}
// GenerateResourceConfig implements providers.Interface
func (p *erroredProvider) GenerateResourceConfig(req providers.GenerateResourceConfigRequest) providers.GenerateResourceConfigResponse {
panic("not implemented")
}
// OpenEphemeralResource implements providers.Interface.
func (p *erroredProvider) OpenEphemeralResource(providers.OpenEphemeralResourceRequest) providers.OpenEphemeralResourceResponse {
var diags tfdiags.Diagnostics

@ -161,6 +161,11 @@ func (o *offlineProvider) ReadResource(_ providers.ReadResourceRequest) provider
}
}
// GenerateResourceConfig implements providers.Interface
func (p *offlineProvider) GenerateResourceConfig(req providers.GenerateResourceConfigRequest) providers.GenerateResourceConfigResponse {
panic("not implemented")
}
func (o *offlineProvider) PlanResourceChange(_ providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.AttributeValue(

@ -121,6 +121,11 @@ func (u *unknownProvider) ReadResource(request providers.ReadResourceRequest) pr
}
}
// GenerateResourceConfig implements providers.Interface
func (p *unknownProvider) GenerateResourceConfig(req providers.GenerateResourceConfigRequest) providers.GenerateResourceConfigResponse {
panic("not implemented")
}
func (u *unknownProvider) PlanResourceChange(request providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
if request.ClientCapabilities.DeferralAllowed {
// For PlanResourceChange, we'll kind of abuse the mocking library to

@ -972,6 +972,114 @@ import {
})
}
// Generate configuration based on the provider's supplied config value
func TestContext2Plan_importResourceProviderConfigGen(t *testing.T) {
addr := mustResourceInstanceAddr("test_object.a")
m := testModuleInline(t, map[string]string{
"main.tf": `
import {
to = test_object.a
id = "123"
}
`,
})
p := simpleMockProvider()
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
Provider: providers.Schema{Body: simpleTestSchema()},
ResourceTypes: map[string]providers.Schema{
"test_object": providers.Schema{Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
// a real computed+optional id attribute, not like the SDK
"id": {
Type: cty.String,
Computed: true,
Optional: true,
},
"is_default": {
Type: cty.String,
Optional: true,
Computed: true,
},
"identifier": {
Type: cty.Number,
Computed: true,
},
"required": {
Type: cty.String,
Required: true,
},
},
}},
},
ServerCapabilities: providers.ServerCapabilities{
GenerateResourceConfig: true,
},
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
p.GenerateResourceConfigResponse = &providers.GenerateResourceConfigResponse{
Config: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("not_the_default"),
"is_default": cty.NullVal(cty.String),
"identifier": cty.NullVal(cty.String),
"required": cty.StringVal("for_config"),
}),
}
p.ReadResourceResponse = &providers.ReadResourceResponse{
NewState: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("not_the_default"),
"is_default": cty.StringVal("default"),
"identifier": cty.StringVal("123456789"),
"required": cty.StringVal("for_config"),
}),
}
p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{
ImportedResources: []providers.ImportedResource{
{
TypeName: "test_object",
State: cty.ObjectVal(map[string]cty.Value{
"id": cty.NullVal(cty.String),
"identifier": cty.StringVal("123456789"),
"is_default": cty.NullVal(cty.String),
"required": cty.NullVal(cty.String),
}),
},
},
}
diags := ctx.Validate(m, &ValidateOpts{})
if diags.HasErrors() {
t.Fatalf("unexpected errors\n%s", diags.Err().Error())
}
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
GenerateConfigPath: "generated.tf", // Actual value here doesn't matter, as long as it is not empty.
})
if diags.HasErrors() {
t.Fatalf("unexpected errors\n%s", diags.Err().Error())
}
instPlan := plan.Changes.ResourceInstance(addr)
if instPlan == nil {
t.Fatalf("no plan for %s at all", addr)
}
want := `resource "test_object" "a" {
id = "not_the_default"
required = "for_config"
}`
got := instPlan.GeneratedConfig
if diff := cmp.Diff(want, got); len(diff) > 0 {
t.Errorf("got:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff)
}
}
func TestContext2Plan_importResourceConfigGenWithAlias(t *testing.T) {
addr := mustResourceInstanceAddr("test_object.a")
m := testModuleInline(t, map[string]string{

@ -967,6 +967,7 @@ var (
provider = test
instance_type = "ami-123456"
}
import {
to = test_resource.test_0
provider = test
@ -979,6 +980,7 @@ resource "test_resource" "test_1" {
provider = test
instance_type = "ami-654321"
}
import {
to = test_resource.test_1
provider = test
@ -991,6 +993,7 @@ resource "test_resource" "test_2" {
provider = test
instance_type = "ami-789012"
}
import {
to = test_resource.test_2
provider = test
@ -998,12 +1001,14 @@ import {
id = "i-v3"
}
}
`
testResourceCfg2 = `resource "test_resource" "test2_0" {
provider = test
instance_type = "ami-123456"
}
import {
to = test_resource.test2_0
provider = test
@ -1016,6 +1021,7 @@ resource "test_resource" "test2_1" {
provider = test
instance_type = "ami-654321"
}
import {
to = test_resource.test2_1
provider = test
@ -1028,6 +1034,7 @@ resource "test_resource" "test2_2" {
provider = test
instance_type = "ami-789012"
}
import {
to = test_resource.test2_2
provider = test
@ -1035,5 +1042,6 @@ import {
id = "i-v3"
}
}
`
)

@ -838,7 +838,7 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs.
}
// Generate the HCL string first, then parse the HCL body from it.
generatedResource, generatedDiags := n.generateHCLResourceDef(n.Addr, instanceRefreshState.Value, schema)
generatedResource, generatedDiags := n.generateHCLResourceDef(ctx, n.Addr, instanceRefreshState.Value)
diags = diags.Append(generatedDiags)
// This wraps the content of the resource block in an enclosing resource block
@ -884,21 +884,121 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs.
// generateHCLResourceDef generates the HCL definition for the resource
// instance, including the surrounding block. This is used to generate the
// configuration for the resource instance when importing or generating
func (n *NodePlannableResourceInstance) generateHCLResourceDef(addr addrs.AbsResourceInstance, state cty.Value, schema providers.Schema) (*genconfig.Resource, tfdiags.Diagnostics) {
func (n *NodePlannableResourceInstance) generateHCLResourceDef(ctx EvalContext, addr addrs.AbsResourceInstance, state cty.Value) (genconfig.Resource, tfdiags.Diagnostics) {
providerAddr := addrs.LocalProviderConfig{
LocalName: n.ResolvedProvider.Provider.Type,
Alias: n.ResolvedProvider.Alias,
}
switch addr.Resource.Resource.Mode {
case addrs.ManagedResourceMode:
return genconfig.GenerateResourceContents(addr, schema.Body, providerAddr, state, false)
case addrs.ListResourceMode:
identitySchema := schema.Identity
return genconfig.GenerateListResourceContents(addr, schema.Body, identitySchema, providerAddr, state)
default:
panic(fmt.Sprintf("unexpected resource mode %s for resource %s", addr.Resource.Resource.Mode, addr))
var diags tfdiags.Diagnostics
providerSchema, err := ctx.ProviderSchema(n.ResolvedProvider)
if err != nil {
return genconfig.Resource{}, diags.Append(err)
}
schema := providerSchema.SchemaForResourceAddr(n.Addr.Resource.Resource)
if schema.Body == nil {
// Should be caught during validation, so we don't bother with a pretty error here
diags = diags.Append(fmt.Errorf("provider does not support resource type for %q", n.Addr))
return genconfig.Resource{}, diags
}
config, genDiags := n.generateResourceConfig(ctx, state)
diags = diags.Append(genDiags)
if diags.HasErrors() {
return genconfig.Resource{}, diags
}
return genconfig.GenerateResourceContents(addr, schema.Body, providerAddr, config, false)
}
func (n *NodePlannableResourceInstance) generateHCLListResourceDef(ctx EvalContext, addr addrs.AbsResourceInstance, state cty.Value) (genconfig.ImportGroup, tfdiags.Diagnostics) {
providerAddr := addrs.LocalProviderConfig{
LocalName: n.ResolvedProvider.Provider.Type,
Alias: n.ResolvedProvider.Alias,
}
var diags tfdiags.Diagnostics
providerSchema, err := ctx.ProviderSchema(n.ResolvedProvider)
if err != nil {
return genconfig.ImportGroup{}, diags.Append(err)
}
schema := providerSchema.ResourceTypes[n.Addr.Resource.Resource.Type]
if schema.Body == nil {
// Should be caught during validation, so we don't bother with a pretty error here
diags = diags.Append(fmt.Errorf("provider does not support resource type for %q", n.Addr))
return genconfig.ImportGroup{}, diags
}
if !state.CanIterateElements() {
panic(fmt.Sprintf("invalid list resource data: %#v\n", state))
}
var listElements []genconfig.ResourceListElement
iter := state.ElementIterator()
for iter.Next() {
_, val := iter.Element()
// we still need to generate the resource block even if the state is not given,
// so that the import block can reference it.
stateVal := cty.NullVal(schema.Body.ImpliedType())
if val.Type().HasAttribute("state") {
stateVal = val.GetAttr("state")
}
config, genDiags := n.generateResourceConfig(ctx, stateVal)
diags = diags.Append(genDiags)
if diags.HasErrors() {
return genconfig.ImportGroup{}, diags
}
idVal := val.GetAttr("identity")
listElements = append(listElements, genconfig.ResourceListElement{Config: config, Identity: idVal})
}
return genconfig.GenerateListResourceContents(addr, schema.Body, schema.Identity, providerAddr, listElements)
}
func (n *NodePlannableResourceInstance) generateResourceConfig(ctx EvalContext, state cty.Value) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
// There should be no marks when generating config, because this is entirely
// new config being generated. We already have the schema for any relevant
// metadata.
state, _ = state.UnmarkDeep()
provider, providerSchema, err := getProvider(ctx, n.ResolvedProvider)
diags = diags.Append(err)
if diags.HasErrors() {
return cty.DynamicVal, diags
}
schema := providerSchema.SchemaForResourceAddr(n.Addr.Resource.Resource)
if schema.Body == nil {
// Should be caught during validation, so we don't bother with a pretty error here
diags = diags.Append(fmt.Errorf("provider does not support resource type for %q", n.Addr))
return cty.DynamicVal, diags
}
// Use the config value from providers which can generate it themselves
if providerSchema.ServerCapabilities.GenerateResourceConfig {
req := providers.GenerateResourceConfigRequest{
TypeName: n.Addr.Resource.Resource.Type,
State: state,
}
resp := provider.GenerateResourceConfig(req)
diags = diags.Append(resp.Diagnostics)
if diags.HasErrors() {
return cty.DynamicVal, diags
}
return resp.Config, diags
}
// or fallback to the default process of guessing at a legacy config.
return genconfig.ExtractLegacyConfigFromState(schema.Body, state), diags
}
// mergeDeps returns the union of 2 sets of dependencies

@ -106,7 +106,7 @@ func (n *NodePlannableResourceInstance) listResourceExecute(ctx EvalContext) (di
// If a path is specified, generate the config for the resource
if n.generateConfigPath != "" {
var gDiags tfdiags.Diagnostics
results.Generated, gDiags = n.generateHCLResourceDef(addr, resp.Result.GetAttr("data"), providerSchema.ResourceTypes[n.Config.Type])
results.Generated, gDiags = n.generateHCLListResourceDef(ctx, addr, resp.Result.GetAttr("data"))
diags = diags.Append(gDiags)
if diags.HasErrors() {
return diags

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save