Skip to content

Commit 6c3be0c

Browse files
committed
Merge remote-tracking branch 'origin/main' into break-dependency-between-statemigrate-and-command
2 parents 87e6f51 + 5fcc451 commit 6c3be0c

15 files changed

+545
-220
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: ENHANCEMENTS
2+
body: '`import` blocks: Now support importing a resource via a new identity attribute. This is mutually exclusive with the `id` attribute'
3+
time: 2025-04-17T18:20:36.814657+02:00
4+
custom:
5+
Issue: "36703"

CODEOWNERS

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# cannot be used automatically by GitHub's pull request workflow and would
1313
# make GitHub consider this file invalid if not commented.
1414

15-
# Remote-state backend # Maintainer
15+
# Remote-state backend # Maintainer
1616
/internal/backend/remote-state/azure @hashicorp/terraform-core @hashicorp/terraform-azure
1717
#/internal/backend/remote-state/consul Unmaintained
1818
#/internal/backend/remote-state/cos @likexian
@@ -22,7 +22,7 @@
2222
#/internal/backend/remote-state/pg @remilapeyre
2323
/internal/backend/remote-state/s3 @hashicorp/terraform-core @hashicorp/terraform-aws
2424
/internal/backend/remote-state/kubernetes @hashicorp/terraform-core @hashicorp/tf-eco-hybrid-cloud
25-
#/internal/backend/remote-state/oracle_oci @ravinitp @pvkrishnachaitanya
25+
#/internal/backend/remote-state/oci @ravinitp @pvkrishnachaitanya
2626

2727
# Cloud backend
2828
/internal/backend/remote @hashicorp/terraform-core @hashicorp/tf-core-cloud

internal/command/jsonplan/plan.go

+49-1
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,11 @@ type Change struct {
156156
// might change in the future. However, not all Importing changes will
157157
// contain generated config.
158158
GeneratedConfig string `json:"generated_config,omitempty"`
159+
160+
// BeforeIdentity and AfterIdentity are representations of the resource
161+
// identity value both before and after the action.
162+
BeforeIdentity json.RawMessage `json:"before_identity,omitempty"`
163+
AfterIdentity json.RawMessage `json:"after_identity,omitempty"`
159164
}
160165

161166
// Importing is a nested object for the resource import metadata.
@@ -168,6 +173,10 @@ type Importing struct {
168173
// would have led to the overall change being deferred, as such this should
169174
// only be true when processing changes from the deferred changes list.
170175
Unknown bool `json:"unknown,omitempty"`
176+
177+
// The identity can be used instead of the ID to target the resource as part
178+
// of the planned import operation.
179+
Identity json.RawMessage `json:"identity,omitempty"`
171180
}
172181

173182
type output struct {
@@ -501,7 +510,44 @@ func marshalResourceChange(rc *plans.ResourceInstanceChangeSrc, schemas *terrafo
501510
if rc.Importing.Unknown {
502511
importing = &Importing{Unknown: true}
503512
} else {
504-
importing = &Importing{ID: rc.Importing.ID}
513+
if rc.Importing.ID != "" {
514+
importing = &Importing{ID: rc.Importing.ID}
515+
} else {
516+
identity, err := rc.Importing.Identity.Decode(schema.Identity.ImpliedType())
517+
if err != nil {
518+
return r, err
519+
}
520+
rawIdentity, err := ctyjson.Marshal(identity, identity.Type())
521+
if err != nil {
522+
return r, err
523+
}
524+
525+
importing = &Importing{
526+
Identity: json.RawMessage(rawIdentity),
527+
}
528+
}
529+
}
530+
}
531+
532+
var beforeIdentity, afterIdentity []byte
533+
if schema.Identity != nil && rc.BeforeIdentity != nil {
534+
identity, err := rc.BeforeIdentity.Decode(schema.Identity.ImpliedType())
535+
if err != nil {
536+
return r, err
537+
}
538+
beforeIdentity, err = ctyjson.Marshal(identity, identity.Type())
539+
if err != nil {
540+
return r, err
541+
}
542+
}
543+
if schema.Identity != nil && rc.AfterIdentity != nil {
544+
identity, err := rc.AfterIdentity.Decode(schema.Identity.ImpliedType())
545+
if err != nil {
546+
return r, err
547+
}
548+
afterIdentity, err = ctyjson.Marshal(identity, identity.Type())
549+
if err != nil {
550+
return r, err
505551
}
506552
}
507553

@@ -515,6 +561,8 @@ func marshalResourceChange(rc *plans.ResourceInstanceChangeSrc, schemas *terrafo
515561
ReplacePaths: replacePaths,
516562
Importing: importing,
517563
GeneratedConfig: rc.GeneratedConfig,
564+
BeforeIdentity: json.RawMessage(beforeIdentity),
565+
AfterIdentity: json.RawMessage(afterIdentity),
518566
}
519567

520568
if rc.DeposedKey != states.NotDeposed {

internal/command/jsonplan/resource.go

+10
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ type resource struct {
4242
// SensitiveValues is similar to AttributeValues, but with all sensitive
4343
// values replaced with true, and all non-sensitive leaf values omitted.
4444
SensitiveValues json.RawMessage `json:"sensitive_values,omitempty"`
45+
46+
// The version of the resource identity schema the "identity" property
47+
// conforms to.
48+
// It's a pointer, because it should be optional, but also 0 is a valid
49+
// schema version.
50+
IdentitySchemaVersion *uint64 `json:"identity_schema_version,omitempty"`
51+
52+
// The JSON representation of the resource identity, whose structure
53+
// depends on the resource identity schema.
54+
IdentityValues attributeValues `json:"identity,omitempty"`
4555
}
4656

4757
// ResourceChange is a description of an individual change action that Terraform

internal/command/jsonplan/values.go

+9-4
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313

1414
"github.com/hashicorp/terraform/internal/addrs"
1515
"github.com/hashicorp/terraform/internal/command/jsonstate"
16-
"github.com/hashicorp/terraform/internal/configs/configschema"
1716
"github.com/hashicorp/terraform/internal/plans"
1817
"github.com/hashicorp/terraform/internal/states"
1918
"github.com/hashicorp/terraform/internal/terraform"
@@ -30,7 +29,7 @@ type stateValues struct {
3029
// resource, whose structure depends on the resource type schema.
3130
type attributeValues map[string]interface{}
3231

33-
func marshalAttributeValues(value cty.Value, schema *configschema.Block) attributeValues {
32+
func marshalAttributeValues(value cty.Value) attributeValues {
3433
if value == cty.NilVal || value.IsNull() {
3534
return nil
3635
}
@@ -220,10 +219,10 @@ func marshalPlanResources(changes *plans.ChangesSrc, ris []addrs.AbsResourceInst
220219

221220
if changeV.After != cty.NilVal {
222221
if changeV.After.IsWhollyKnown() {
223-
resource.AttributeValues = marshalAttributeValues(changeV.After, schema.Body)
222+
resource.AttributeValues = marshalAttributeValues(changeV.After)
224223
} else {
225224
knowns := omitUnknowns(changeV.After)
226-
resource.AttributeValues = marshalAttributeValues(knowns, schema.Body)
225+
resource.AttributeValues = marshalAttributeValues(knowns)
227226
}
228227
}
229228

@@ -234,6 +233,12 @@ func marshalPlanResources(changes *plans.ChangesSrc, ris []addrs.AbsResourceInst
234233
}
235234
resource.SensitiveValues = v
236235

236+
if schema.Identity != nil && !changeV.AfterIdentity.IsNull() {
237+
identityVersion := uint64(schema.IdentityVersion)
238+
resource.IdentitySchemaVersion = &identityVersion
239+
resource.IdentityValues = marshalAttributeValues(changeV.AfterIdentity)
240+
}
241+
237242
ret = append(ret, resource)
238243
}
239244

internal/command/jsonplan/values_test.go

+72-9
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ import (
1717
"github.com/hashicorp/terraform/internal/terraform"
1818
)
1919

20+
func ptrOf[T any](v T) *T {
21+
return &v
22+
}
23+
2024
func TestMarshalAttributeValues(t *testing.T) {
2125
tests := []struct {
2226
Attr cty.Value
@@ -105,7 +109,7 @@ func TestMarshalAttributeValues(t *testing.T) {
105109
}
106110

107111
for _, test := range tests {
108-
got := marshalAttributeValues(test.Attr, test.Schema)
112+
got := marshalAttributeValues(test.Attr)
109113
eq := reflect.DeepEqual(got, test.Want)
110114
if !eq {
111115
t.Fatalf("wrong result:\nGot: %#v\nWant: %#v\n", got, test.Want)
@@ -185,11 +189,13 @@ func TestMarshalPlannedOutputs(t *testing.T) {
185189

186190
func TestMarshalPlanResources(t *testing.T) {
187191
tests := map[string]struct {
188-
Action plans.Action
189-
Before cty.Value
190-
After cty.Value
191-
Want []resource
192-
Err bool
192+
Action plans.Action
193+
Before cty.Value
194+
After cty.Value
195+
Want []resource
196+
Err bool
197+
BeforeIdentity cty.Value
198+
AfterIdentity cty.Value
193199
}{
194200
"create with unknowns": {
195201
Action: plans.Create,
@@ -257,6 +263,37 @@ func TestMarshalPlanResources(t *testing.T) {
257263
}},
258264
Err: false,
259265
},
266+
"with identity": {
267+
Action: plans.Create,
268+
Before: cty.NullVal(cty.EmptyObject),
269+
After: cty.ObjectVal(map[string]cty.Value{
270+
"woozles": cty.StringVal("woo"),
271+
"foozles": cty.NullVal(cty.String),
272+
}),
273+
BeforeIdentity: cty.NullVal(cty.EmptyObject),
274+
AfterIdentity: cty.ObjectVal(map[string]cty.Value{
275+
"id": cty.StringVal("someId"),
276+
}),
277+
Want: []resource{{
278+
Address: "test_thing.example",
279+
Mode: "managed",
280+
Type: "test_thing",
281+
Name: "example",
282+
Index: addrs.InstanceKey(nil),
283+
ProviderName: "registry.terraform.io/hashicorp/test",
284+
SchemaVersion: 1,
285+
AttributeValues: attributeValues{
286+
"woozles": json.RawMessage(`"woo"`),
287+
"foozles": json.RawMessage(`null`),
288+
},
289+
SensitiveValues: json.RawMessage("{}"),
290+
IdentitySchemaVersion: ptrOf[uint64](2),
291+
IdentityValues: attributeValues{
292+
"id": json.RawMessage(`"someId"`),
293+
},
294+
}},
295+
Err: false,
296+
},
260297
}
261298

262299
for name, test := range tests {
@@ -270,6 +307,23 @@ func TestMarshalPlanResources(t *testing.T) {
270307
if err != nil {
271308
t.Fatal(err)
272309
}
310+
311+
var beforeIdentity, afterIdentity plans.DynamicValue
312+
if !test.BeforeIdentity.IsNull() {
313+
var err error
314+
beforeIdentity, err = plans.NewDynamicValue(test.BeforeIdentity, test.BeforeIdentity.Type())
315+
if err != nil {
316+
t.Fatal(err)
317+
}
318+
}
319+
if !test.AfterIdentity.IsNull() {
320+
var err error
321+
afterIdentity, err = plans.NewDynamicValue(test.AfterIdentity, test.AfterIdentity.Type())
322+
if err != nil {
323+
t.Fatal(err)
324+
}
325+
}
326+
273327
testChange := &plans.ChangesSrc{
274328
Resources: []*plans.ResourceInstanceChangeSrc{
275329
{
@@ -283,9 +337,11 @@ func TestMarshalPlanResources(t *testing.T) {
283337
Module: addrs.RootModule,
284338
},
285339
ChangeSrc: plans.ChangeSrc{
286-
Action: test.Action,
287-
Before: before,
288-
After: after,
340+
Action: test.Action,
341+
Before: before,
342+
After: after,
343+
BeforeIdentity: beforeIdentity,
344+
AfterIdentity: afterIdentity,
289345
},
290346
},
291347
},
@@ -357,6 +413,13 @@ func testSchemas() *terraform.Schemas {
357413
"foozles": {Type: cty.String, Optional: true},
358414
},
359415
},
416+
IdentityVersion: 2,
417+
Identity: &configschema.Object{
418+
Attributes: map[string]*configschema.Attribute{
419+
"id": {Type: cty.String, Required: true},
420+
},
421+
Nesting: configschema.NestingSingle,
422+
},
360423
},
361424
},
362425
},

internal/plans/changes.go

+7-3
Original file line numberDiff line numberDiff line change
@@ -556,14 +556,18 @@ type Importing struct {
556556

557557
// Encode converts the Importing object into a form suitable for serialization
558558
// to a plan file.
559-
func (i *Importing) Encode() *ImportingSrc {
559+
func (i *Importing) Encode(identityTy cty.Type) *ImportingSrc {
560560
if i == nil {
561561
return nil
562562
}
563563
if i.Target.IsWhollyKnown() {
564564
if i.Target.Type().IsObjectType() {
565+
identity, err := NewDynamicValue(i.Target, identityTy)
566+
if err != nil {
567+
return nil
568+
}
565569
return &ImportingSrc{
566-
Identity: i.Target,
570+
Identity: identity,
567571
}
568572
} else {
569573
return &ImportingSrc{
@@ -692,7 +696,7 @@ func (c *Change) Encode(schema *providers.Schema) (*ChangeSrc, error) {
692696
AfterSensitivePaths: sensitiveAttrsAfter,
693697
BeforeIdentity: beforeIdentityDV,
694698
AfterIdentity: afterIdentityDV,
695-
Importing: c.Importing.Encode(),
699+
Importing: c.Importing.Encode(identityTy),
696700
GeneratedConfig: c.GeneratedConfig,
697701
}, nil
698702
}

internal/plans/changes_src.go

+12-7
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ type ImportingSrc struct {
350350
ID string
351351

352352
// Identity is the original identity of the imported resource.
353-
Identity cty.Value
353+
Identity DynamicValue
354354

355355
// Unknown is true if the ID was unknown when we tried to import it. This
356356
// should only be true if the overall change is embedded within a deferred
@@ -359,12 +359,12 @@ type ImportingSrc struct {
359359
}
360360

361361
// Decode unmarshals the raw representation of the importing action.
362-
func (is *ImportingSrc) Decode() *Importing {
362+
func (is *ImportingSrc) Decode(identityType cty.Type) *Importing {
363363
if is == nil {
364364
return nil
365365
}
366366
if is.Unknown {
367-
if is.Identity.IsNull() {
367+
if is.Identity == nil {
368368
return &Importing{
369369
Target: cty.UnknownVal(cty.String),
370370
}
@@ -375,15 +375,20 @@ func (is *ImportingSrc) Decode() *Importing {
375375
}
376376
}
377377

378-
if is.Identity.IsNull() {
378+
if is.Identity == nil {
379379
return &Importing{
380380
Target: cty.StringVal(is.ID),
381381
}
382382
}
383383

384-
return &Importing{
385-
Target: is.Identity,
384+
target, err := is.Identity.Decode(identityType)
385+
if err != nil {
386+
return &Importing{
387+
Target: target,
388+
}
386389
}
390+
391+
return nil
387392
}
388393

389394
// ChangeSrc is a not-yet-decoded Change.
@@ -476,7 +481,7 @@ func (cs *ChangeSrc) Decode(schema *providers.Schema) (*Change, error) {
476481
BeforeIdentity: beforeIdentity,
477482
After: marks.MarkPaths(after, marks.Sensitive, cs.AfterSensitivePaths),
478483
AfterIdentity: afterIdentity,
479-
Importing: cs.Importing.Decode(),
484+
Importing: cs.Importing.Decode(identityType),
480485
GeneratedConfig: cs.GeneratedConfig,
481486
}, nil
482487
}

0 commit comments

Comments
 (0)