Skip to content
Merged
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
50 changes: 49 additions & 1 deletion internal/command/jsonplan/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,11 @@ type Change struct {
// might change in the future. However, not all Importing changes will
// contain generated config.
GeneratedConfig string `json:"generated_config,omitempty"`

// BeforeIdentity and AfterIdentity are representations of the resource
// identity value both before and after the action.
BeforeIdentity json.RawMessage `json:"before_identity,omitempty"`
AfterIdentity json.RawMessage `json:"after_identity,omitempty"`
}

// Importing is a nested object for the resource import metadata.
Expand All @@ -168,6 +173,10 @@ type Importing struct {
// would have led to the overall change being deferred, as such this should
// only be true when processing changes from the deferred changes list.
Unknown bool `json:"unknown,omitempty"`

// The identity can be used instead of the ID to target the resource as part
// of the planned import operation.
Identity json.RawMessage `json:"identity,omitempty"`
}

type output struct {
Expand Down Expand Up @@ -501,7 +510,44 @@ func marshalResourceChange(rc *plans.ResourceInstanceChangeSrc, schemas *terrafo
if rc.Importing.Unknown {
importing = &Importing{Unknown: true}
} else {
importing = &Importing{ID: rc.Importing.ID}
if rc.Importing.ID != "" {
importing = &Importing{ID: rc.Importing.ID}
} else {
identity, err := rc.Importing.Identity.Decode(schema.Identity.ImpliedType())
if err != nil {
return r, err
}
rawIdentity, err := ctyjson.Marshal(identity, identity.Type())
if err != nil {
return r, err
}

importing = &Importing{
Identity: json.RawMessage(rawIdentity),
}
}
}
}

var beforeIdentity, afterIdentity []byte
if schema.Identity != nil && rc.BeforeIdentity != nil {
identity, err := rc.BeforeIdentity.Decode(schema.Identity.ImpliedType())
if err != nil {
return r, err
}
beforeIdentity, err = ctyjson.Marshal(identity, identity.Type())
if err != nil {
return r, err
}
}
if schema.Identity != nil && rc.AfterIdentity != nil {
identity, err := rc.AfterIdentity.Decode(schema.Identity.ImpliedType())
if err != nil {
return r, err
}
afterIdentity, err = ctyjson.Marshal(identity, identity.Type())
if err != nil {
return r, err
}
}

Expand All @@ -515,6 +561,8 @@ func marshalResourceChange(rc *plans.ResourceInstanceChangeSrc, schemas *terrafo
ReplacePaths: replacePaths,
Importing: importing,
GeneratedConfig: rc.GeneratedConfig,
BeforeIdentity: json.RawMessage(beforeIdentity),
AfterIdentity: json.RawMessage(afterIdentity),
}

if rc.DeposedKey != states.NotDeposed {
Expand Down
10 changes: 10 additions & 0 deletions internal/command/jsonplan/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ type resource struct {
// SensitiveValues is similar to AttributeValues, but with all sensitive
// values replaced with true, and all non-sensitive leaf values omitted.
SensitiveValues json.RawMessage `json:"sensitive_values,omitempty"`

// The version of the resource identity schema the "identity" property
// conforms to.
// It's a pointer, because it should be optional, but also 0 is a valid
// schema version.
IdentitySchemaVersion *uint64 `json:"identity_schema_version,omitempty"`

// The JSON representation of the resource identity, whose structure
// depends on the resource identity schema.
IdentityValues attributeValues `json:"identity,omitempty"`
}

// ResourceChange is a description of an individual change action that Terraform
Expand Down
13 changes: 9 additions & 4 deletions internal/command/jsonplan/values.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (

"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/command/jsonstate"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/terraform"
Expand All @@ -30,7 +29,7 @@ type stateValues struct {
// resource, whose structure depends on the resource type schema.
type attributeValues map[string]interface{}

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

if changeV.After != cty.NilVal {
if changeV.After.IsWhollyKnown() {
resource.AttributeValues = marshalAttributeValues(changeV.After, schema.Body)
resource.AttributeValues = marshalAttributeValues(changeV.After)
} else {
knowns := omitUnknowns(changeV.After)
resource.AttributeValues = marshalAttributeValues(knowns, schema.Body)
resource.AttributeValues = marshalAttributeValues(knowns)
}
}

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

if schema.Identity != nil && !changeV.AfterIdentity.IsNull() {
identityVersion := uint64(schema.IdentityVersion)
resource.IdentitySchemaVersion = &identityVersion
resource.IdentityValues = marshalAttributeValues(changeV.AfterIdentity)
}

ret = append(ret, resource)
}

Expand Down
81 changes: 72 additions & 9 deletions internal/command/jsonplan/values_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import (
"github.com/hashicorp/terraform/internal/terraform"
)

func ptrOf[T any](v T) *T {
return &v
}

func TestMarshalAttributeValues(t *testing.T) {
tests := []struct {
Attr cty.Value
Expand Down Expand Up @@ -105,7 +109,7 @@ func TestMarshalAttributeValues(t *testing.T) {
}

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

func TestMarshalPlanResources(t *testing.T) {
tests := map[string]struct {
Action plans.Action
Before cty.Value
After cty.Value
Want []resource
Err bool
Action plans.Action
Before cty.Value
After cty.Value
Want []resource
Err bool
BeforeIdentity cty.Value
AfterIdentity cty.Value
}{
"create with unknowns": {
Action: plans.Create,
Expand Down Expand Up @@ -257,6 +263,37 @@ func TestMarshalPlanResources(t *testing.T) {
}},
Err: false,
},
"with identity": {
Action: plans.Create,
Before: cty.NullVal(cty.EmptyObject),
After: cty.ObjectVal(map[string]cty.Value{
"woozles": cty.StringVal("woo"),
"foozles": cty.NullVal(cty.String),
}),
BeforeIdentity: cty.NullVal(cty.EmptyObject),
AfterIdentity: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("someId"),
}),
Want: []resource{{
Address: "test_thing.example",
Mode: "managed",
Type: "test_thing",
Name: "example",
Index: addrs.InstanceKey(nil),
ProviderName: "registry.terraform.io/hashicorp/test",
SchemaVersion: 1,
AttributeValues: attributeValues{
"woozles": json.RawMessage(`"woo"`),
"foozles": json.RawMessage(`null`),
},
SensitiveValues: json.RawMessage("{}"),
IdentitySchemaVersion: ptrOf[uint64](2),
IdentityValues: attributeValues{
"id": json.RawMessage(`"someId"`),
},
}},
Err: false,
},
}

for name, test := range tests {
Expand All @@ -270,6 +307,23 @@ func TestMarshalPlanResources(t *testing.T) {
if err != nil {
t.Fatal(err)
}

var beforeIdentity, afterIdentity plans.DynamicValue
if !test.BeforeIdentity.IsNull() {
var err error
beforeIdentity, err = plans.NewDynamicValue(test.BeforeIdentity, test.BeforeIdentity.Type())
if err != nil {
t.Fatal(err)
}
}
if !test.AfterIdentity.IsNull() {
var err error
afterIdentity, err = plans.NewDynamicValue(test.AfterIdentity, test.AfterIdentity.Type())
if err != nil {
t.Fatal(err)
}
}

testChange := &plans.ChangesSrc{
Resources: []*plans.ResourceInstanceChangeSrc{
{
Expand All @@ -283,9 +337,11 @@ func TestMarshalPlanResources(t *testing.T) {
Module: addrs.RootModule,
},
ChangeSrc: plans.ChangeSrc{
Action: test.Action,
Before: before,
After: after,
Action: test.Action,
Before: before,
After: after,
BeforeIdentity: beforeIdentity,
AfterIdentity: afterIdentity,
},
},
},
Expand Down Expand Up @@ -357,6 +413,13 @@ func testSchemas() *terraform.Schemas {
"foozles": {Type: cty.String, Optional: true},
},
},
IdentityVersion: 2,
Identity: &configschema.Object{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Required: true},
},
Nesting: configschema.NestingSingle,
},
},
},
},
Expand Down
10 changes: 7 additions & 3 deletions internal/plans/changes.go
Original file line number Diff line number Diff line change
Expand Up @@ -556,14 +556,18 @@ type Importing struct {

// Encode converts the Importing object into a form suitable for serialization
// to a plan file.
func (i *Importing) Encode() *ImportingSrc {
func (i *Importing) Encode(identityTy cty.Type) *ImportingSrc {
if i == nil {
return nil
}
if i.Target.IsWhollyKnown() {
if i.Target.Type().IsObjectType() {
identity, err := NewDynamicValue(i.Target, identityTy)
if err != nil {
return nil
}
return &ImportingSrc{
Identity: i.Target,
Identity: identity,
}
} else {
return &ImportingSrc{
Expand Down Expand Up @@ -692,7 +696,7 @@ func (c *Change) Encode(schema *providers.Schema) (*ChangeSrc, error) {
AfterSensitivePaths: sensitiveAttrsAfter,
BeforeIdentity: beforeIdentityDV,
AfterIdentity: afterIdentityDV,
Importing: c.Importing.Encode(),
Importing: c.Importing.Encode(identityTy),
GeneratedConfig: c.GeneratedConfig,
}, nil
}
19 changes: 12 additions & 7 deletions internal/plans/changes_src.go
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ type ImportingSrc struct {
ID string

// Identity is the original identity of the imported resource.
Identity cty.Value
Identity DynamicValue

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

// Decode unmarshals the raw representation of the importing action.
func (is *ImportingSrc) Decode() *Importing {
func (is *ImportingSrc) Decode(identityType cty.Type) *Importing {
if is == nil {
return nil
}
if is.Unknown {
if is.Identity.IsNull() {
if is.Identity == nil {
return &Importing{
Target: cty.UnknownVal(cty.String),
}
Expand All @@ -375,15 +375,20 @@ func (is *ImportingSrc) Decode() *Importing {
}
}

if is.Identity.IsNull() {
if is.Identity == nil {
return &Importing{
Target: cty.StringVal(is.ID),
}
}

return &Importing{
Target: is.Identity,
target, err := is.Identity.Decode(identityType)
if err != nil {
return &Importing{
Target: target,
}
}

return nil
}

// ChangeSrc is a not-yet-decoded Change.
Expand Down Expand Up @@ -476,7 +481,7 @@ func (cs *ChangeSrc) Decode(schema *providers.Schema) (*Change, error) {
BeforeIdentity: beforeIdentity,
After: marks.MarkPaths(after, marks.Sensitive, cs.AfterSensitivePaths),
AfterIdentity: afterIdentity,
Importing: cs.Importing.Decode(),
Importing: cs.Importing.Decode(identityType),
GeneratedConfig: cs.GeneratedConfig,
}, nil
}
Loading