diff --git a/internal/backend/local/testing.go b/internal/backend/local/testing.go index 347252a05367..2b3a4926f6ac 100644 --- a/internal/backend/local/testing.go +++ b/internal/backend/local/testing.go @@ -57,19 +57,19 @@ func TestLocalProvider(t *testing.T, b *Local, name string, schema providers.Pro return resp } - rSchema, _ := schema.SchemaForResourceType(addrs.ManagedResourceMode, req.TypeName) - if rSchema == nil { - rSchema = &configschema.Block{} // default schema is empty + rSchema := schema.SchemaForResourceType(addrs.ManagedResourceMode, req.TypeName) + if rSchema.Body == nil { + rSchema.Body = &configschema.Block{} // default schema is empty } plannedVals := map[string]cty.Value{} - for name, attrS := range rSchema.Attributes { + for name, attrS := range rSchema.Body.Attributes { val := req.ProposedNewState.GetAttr(name) if attrS.Computed && val.IsNull() { val = cty.UnknownVal(attrS.Type) } plannedVals[name] = val } - for name := range rSchema.BlockTypes { + for name := range rSchema.Body.BlockTypes { // For simplicity's sake we just copy the block attributes over // verbatim, since this package's mock providers are all relatively // simple -- we're testing the backend, not esoteric provider features. @@ -99,7 +99,6 @@ func TestLocalProvider(t *testing.T, b *Local, name string, schema providers.Pro } return p - } // TestLocalSingleState is a backend implementation that wraps Local diff --git a/internal/builtin/providers/terraform/provider.go b/internal/builtin/providers/terraform/provider.go index 300bd81d958a..6ee3041b5b10 100644 --- a/internal/builtin/providers/terraform/provider.go +++ b/internal/builtin/providers/terraform/provider.go @@ -77,6 +77,14 @@ func (p *Provider) GetProviderSchema() providers.GetProviderSchemaResponse { return resp } +func (p *Provider) GetResourceIdentitySchemas() providers.GetResourceIdentitySchemasResponse { + return providers.GetResourceIdentitySchemasResponse{ + IdentityTypes: map[string]providers.IdentitySchema{ + "terraform_data": dataStoreResourceIdentitySchema(), + }, + } +} + // ValidateProviderConfig is used to validate the configuration values. func (p *Provider) ValidateProviderConfig(req providers.ValidateProviderConfigRequest) providers.ValidateProviderConfigResponse { // At this moment there is nothing to configure for the terraform provider, @@ -150,6 +158,10 @@ func (p *Provider) UpgradeResourceState(req providers.UpgradeResourceStateReques return upgradeDataStoreResourceState(req) } +func (p *Provider) UpgradeResourceIdentity(req providers.UpgradeResourceIdentityRequest) providers.UpgradeResourceIdentityResponse { + return upgradeDataStoreResourceIdentity(req) +} + // ReadResource refreshes a resource and returns its current state. func (p *Provider) ReadResource(req providers.ReadResourceRequest) providers.ReadResourceResponse { return readDataStoreResourceState(req) diff --git a/internal/builtin/providers/terraform/resource_data.go b/internal/builtin/providers/terraform/resource_data.go index e1db0d80e24e..20d312254945 100644 --- a/internal/builtin/providers/terraform/resource_data.go +++ b/internal/builtin/providers/terraform/resource_data.go @@ -25,6 +25,23 @@ func dataStoreResourceSchema() providers.Schema { "id": {Type: cty.String, Computed: true}, }, }, + Identity: dataStoreResourceIdentitySchema().Body, + } +} + +func dataStoreResourceIdentitySchema() providers.IdentitySchema { + return providers.IdentitySchema{ + Version: 0, + Body: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Description: "The unique identifier for the data store.", + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, } } @@ -55,6 +72,11 @@ func upgradeDataStoreResourceState(req providers.UpgradeResourceStateRequest) (r return resp } +func upgradeDataStoreResourceIdentity(providers.UpgradeResourceIdentityRequest) (resp providers.UpgradeResourceIdentityResponse) { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("The builtin provider does not support provider upgrades since it has not changed the identity schema yet.")) + return resp +} + func readDataStoreResourceState(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { resp.NewState = req.PriorState return resp diff --git a/internal/command/jsonconfig/config.go b/internal/command/jsonconfig/config.go index 65518a7da946..372f64ce7652 100644 --- a/internal/command/jsonconfig/config.go +++ b/internal/command/jsonconfig/config.go @@ -472,17 +472,17 @@ func marshalResources(resources map[string]*configs.Resource, schemas *terraform } } - schema, schemaVer := schemas.ResourceTypeConfig( + schema := schemas.ResourceTypeConfig( v.Provider, v.Mode, v.Type, ) - if schema == nil { + if schema.Body == nil { return nil, fmt.Errorf("no schema found for %s (in provider %s)", v.Addr().String(), v.Provider) } - r.SchemaVersion = schemaVer + r.SchemaVersion = uint64(schema.Version) - r.Expressions = marshalExpressions(v.Config, schema) + r.Expressions = marshalExpressions(v.Config, schema.Body) // Managed is populated only for Mode = addrs.ManagedResourceMode if v.Managed != nil && len(v.Managed.Provisioners) > 0 { diff --git a/internal/command/jsonplan/plan.go b/internal/command/jsonplan/plan.go index 9ecafa48c0ca..4cf788b70e56 100644 --- a/internal/command/jsonplan/plan.go +++ b/internal/command/jsonplan/plan.go @@ -424,16 +424,16 @@ func marshalResourceChange(rc *plans.ResourceInstanceChangeSrc, schemas *terrafo r.PreviousAddress = rc.PrevRunAddr.String() } - schema, _ := schemas.ResourceTypeConfig( + schema := schemas.ResourceTypeConfig( rc.ProviderAddr.Provider, addr.Resource.Resource.Mode, addr.Resource.Resource.Type, ) - if schema == nil { + if schema.Body == nil { return r, fmt.Errorf("no schema found for %s (in provider %s)", r.Address, rc.ProviderAddr.Provider) } - changeV, err := rc.Decode(schema.ImpliedType()) + changeV, err := rc.Decode(schema.Body.ImpliedType()) if err != nil { return r, err } @@ -452,7 +452,7 @@ func marshalResourceChange(rc *plans.ResourceInstanceChangeSrc, schemas *terrafo return r, err } sensitivePaths := rc.BeforeSensitivePaths - sensitivePaths = append(sensitivePaths, schema.SensitivePaths(changeV.Before, nil)...) + sensitivePaths = append(sensitivePaths, schema.Body.SensitivePaths(changeV.Before, nil)...) bs := jsonstate.SensitiveAsBool(marks.MarkPaths(changeV.Before, marks.Sensitive, sensitivePaths)) beforeSensitive, err = ctyjson.Marshal(bs, bs.Type()) if err != nil { @@ -479,7 +479,7 @@ func marshalResourceChange(rc *plans.ResourceInstanceChangeSrc, schemas *terrafo afterUnknown = unknownAsBool(changeV.After) } sensitivePaths := rc.AfterSensitivePaths - sensitivePaths = append(sensitivePaths, schema.SensitivePaths(changeV.After, nil)...) + sensitivePaths = append(sensitivePaths, schema.Body.SensitivePaths(changeV.After, nil)...) as := jsonstate.SensitiveAsBool(marks.MarkPaths(changeV.After, marks.Sensitive, sensitivePaths)) afterSensitive, err = ctyjson.Marshal(as, as.Type()) if err != nil { diff --git a/internal/command/jsonplan/values.go b/internal/command/jsonplan/values.go index 37a6f41d9c0e..fded2ef5a49b 100644 --- a/internal/command/jsonplan/values.go +++ b/internal/command/jsonplan/values.go @@ -195,16 +195,16 @@ func marshalPlanResources(changes *plans.ChangesSrc, ris []addrs.AbsResourceInst ) } - schema, schemaVer := schemas.ResourceTypeConfig( + schema := schemas.ResourceTypeConfig( r.ProviderAddr.Provider, r.Addr.Resource.Resource.Mode, resource.Type, ) - if schema == nil { + if schema.Body == nil { return nil, fmt.Errorf("no schema found for %s", r.Addr.String()) } - resource.SchemaVersion = schemaVer - changeV, err := r.Decode(schema.ImpliedType()) + resource.SchemaVersion = uint64(schema.Version) + changeV, err := r.Decode(schema.Body.ImpliedType()) if err != nil { return nil, err } @@ -220,10 +220,10 @@ func marshalPlanResources(changes *plans.ChangesSrc, ris []addrs.AbsResourceInst if changeV.After != cty.NilVal { if changeV.After.IsWhollyKnown() { - resource.AttributeValues = marshalAttributeValues(changeV.After, schema) + resource.AttributeValues = marshalAttributeValues(changeV.After, schema.Body) } else { knowns := omitUnknowns(changeV.After) - resource.AttributeValues = marshalAttributeValues(knowns, schema) + resource.AttributeValues = marshalAttributeValues(knowns, schema.Body) } } diff --git a/internal/command/jsonstate/state.go b/internal/command/jsonstate/state.go index f77da0e036c9..4f2694ca18c6 100644 --- a/internal/command/jsonstate/state.go +++ b/internal/command/jsonstate/state.go @@ -379,7 +379,7 @@ func marshalResources(resources map[string]*states.Resource, module addrs.Module ) } - schema, version := schemas.ResourceTypeConfig( + schema := schemas.ResourceTypeConfig( r.ProviderConfig.Provider, resAddr.Mode, resAddr.Type, @@ -387,16 +387,16 @@ func marshalResources(resources map[string]*states.Resource, module addrs.Module // It is possible that the only instance is deposed if ri.Current != nil { - if version != ri.Current.SchemaVersion { - return nil, fmt.Errorf("schema version %d for %s in state does not match version %d from the provider", ri.Current.SchemaVersion, resAddr, version) + if schema.Version != int64(ri.Current.SchemaVersion) { + return nil, fmt.Errorf("schema version %d for %s in state does not match version %d from the provider", ri.Current.SchemaVersion, resAddr, schema.Version) } current.SchemaVersion = ri.Current.SchemaVersion - if schema == nil { + if schema.Body == nil { return nil, fmt.Errorf("no schema found for %s (in provider %s)", resAddr.String(), r.ProviderConfig.Provider) } - riObj, err := ri.Current.Decode(schema.ImpliedType()) + riObj, err := ri.Current.Decode(schema) if err != nil { return nil, err } @@ -407,7 +407,7 @@ func marshalResources(resources map[string]*states.Resource, module addrs.Module if err != nil { return nil, fmt.Errorf("preparing attribute values for %s: %w", current.Address, err) } - sensitivePaths = append(sensitivePaths, schema.SensitivePaths(value, nil)...) + sensitivePaths = append(sensitivePaths, schema.Body.SensitivePaths(value, nil)...) s := SensitiveAsBool(marks.MarkPaths(value, marks.Sensitive, sensitivePaths)) v, err := ctyjson.Marshal(s, s.Type()) if err != nil { @@ -448,7 +448,7 @@ func marshalResources(resources map[string]*states.Resource, module addrs.Module Index: current.Index, } - riObj, err := rios.Decode(schema.ImpliedType()) + riObj, err := rios.Decode(schema) if err != nil { return nil, err } @@ -459,7 +459,7 @@ func marshalResources(resources map[string]*states.Resource, module addrs.Module if err != nil { return nil, fmt.Errorf("preparing attribute values for %s: %w", current.Address, err) } - sensitivePaths = append(sensitivePaths, schema.SensitivePaths(value, nil)...) + sensitivePaths = append(sensitivePaths, schema.Body.SensitivePaths(value, nil)...) s := SensitiveAsBool(marks.MarkPaths(value, marks.Sensitive, sensitivePaths)) v, err := ctyjson.Marshal(s, s.Type()) if err != nil { diff --git a/internal/command/views/operation_test.go b/internal/command/views/operation_test.go index e63a2fd04fc1..aec9fca1b336 100644 --- a/internal/command/views/operation_test.go +++ b/internal/command/views/operation_test.go @@ -118,12 +118,12 @@ func TestOperation_planNoChanges(t *testing.T) { Type: "test_resource", Name: "somewhere", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - schema, _ := schemas.ResourceTypeConfig( + schema := schemas.ResourceTypeConfig( addrs.NewDefaultProvider("test"), addr.Resource.Resource.Mode, addr.Resource.Resource.Type, ) - ty := schema.ImpliedType() + ty := schema.Body.ImpliedType() rc := &plans.ResourceInstanceChange{ Addr: addr, PrevRunAddr: addr, @@ -159,12 +159,12 @@ func TestOperation_planNoChanges(t *testing.T) { Type: "test_resource", Name: "somewhere", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - schema, _ := schemas.ResourceTypeConfig( + schema := schemas.ResourceTypeConfig( addrs.NewDefaultProvider("test"), addr.Resource.Resource.Mode, addr.Resource.Resource.Type, ) - ty := schema.ImpliedType() + ty := schema.Body.ImpliedType() rc := &plans.ResourceInstanceChange{ Addr: addr, PrevRunAddr: addr, @@ -206,12 +206,12 @@ func TestOperation_planNoChanges(t *testing.T) { Type: "test_resource", Name: "somewhere", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - schema, _ := schemas.ResourceTypeConfig( + schema := schemas.ResourceTypeConfig( addrs.NewDefaultProvider("test"), addr.Resource.Resource.Mode, addr.Resource.Resource.Type, ) - ty := schema.ImpliedType() + ty := schema.Body.ImpliedType() rc := &plans.ResourceInstanceChange{ Addr: addr, PrevRunAddr: addr, @@ -252,12 +252,12 @@ func TestOperation_planNoChanges(t *testing.T) { Type: "test_resource", Name: "anywhere", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - schema, _ := schemas.ResourceTypeConfig( + schema := schemas.ResourceTypeConfig( addrs.NewDefaultProvider("test"), addr.Resource.Resource.Mode, addr.Resource.Resource.Type, ) - ty := schema.ImpliedType() + ty := schema.Body.ImpliedType() rc := &plans.ResourceInstanceChange{ Addr: addr, PrevRunAddr: addrPrev, diff --git a/internal/configs/configschema/internal_validate.go b/internal/configs/configschema/internal_validate.go index 55681cac9ad1..d2343c74b08d 100644 --- a/internal/configs/configschema/internal_validate.go +++ b/internal/configs/configschema/internal_validate.go @@ -171,3 +171,17 @@ func (a *Attribute) internalValidate(name, prefix string) error { return err } + +func (o *Object) InternalValidate() error { + var err error + + for name, attrS := range o.Attributes { + if attrS == nil { + err = errors.Join(err, fmt.Errorf("%s: attribute schema is nil", name)) + continue + } + err = errors.Join(err, attrS.internalValidate(name, "")) + } + + return err +} diff --git a/internal/configs/configschema/internal_validate_test.go b/internal/configs/configschema/internal_validate_test.go index 0f2e4e141be8..f0be0cebc1c2 100644 --- a/internal/configs/configschema/internal_validate_test.go +++ b/internal/configs/configschema/internal_validate_test.go @@ -327,6 +327,98 @@ func TestBlockInternalValidate(t *testing.T) { } } +func TestObjectInternalValidate(t *testing.T) { + tests := map[string]struct { + Object *Object + Errs []string + }{ + "empty": { + &Object{}, + []string{}, + }, + "valid": { + &Object{ + Attributes: map[string]*Attribute{ + "foo": { + Type: cty.String, + Required: true, + }, + "bar": { + Type: cty.String, + Optional: true, + }, + }, + Nesting: NestingSingle, + }, + []string{}, + }, + "nil": { + &Object{ + Attributes: map[string]*Attribute{ + "foo": nil, + }, + Nesting: NestingSingle, + }, + []string{"foo: attribute schema is nil"}, + }, + "attribute with no flags set": { + &Object{ + Attributes: map[string]*Attribute{ + "foo": { + Type: cty.String, + }, + }, + Nesting: NestingSingle, + }, + []string{"foo: must set Optional, Required or Computed"}, + }, + "attribute required and optional": { + &Object{ + Attributes: map[string]*Attribute{ + "foo": { + Type: cty.String, + Required: true, + Optional: true, + }, + }, + Nesting: NestingSingle, + }, + []string{"foo: cannot set both Optional and Required"}, + }, + "attribute with missing type": { + &Object{ + Attributes: map[string]*Attribute{ + "foo": { + Optional: true, + }, + }, + Nesting: NestingSingle, + }, + []string{"foo: either Type or NestedType must be defined"}, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + errs := joinedErrors(test.Object.InternalValidate()) + if got, want := len(errs), len(test.Errs); got != want { + t.Errorf("wrong number of errors %d; want %d", got, want) + for _, err := range errs { + t.Logf("- %s", err.Error()) + } + } else { + if len(errs) > 0 { + for i := range errs { + if errs[i].Error() != test.Errs[i] { + t.Errorf("wrong error: got %s, want %s", errs[i].Error(), test.Errs[i]) + } + } + } + } + }) + } +} + func joinedErrors(err error) []error { if err == nil { return nil diff --git a/internal/grpcwrap/provider.go b/internal/grpcwrap/provider.go index 5648e0e27557..813ce2e2c20b 100644 --- a/internal/grpcwrap/provider.go +++ b/internal/grpcwrap/provider.go @@ -5,6 +5,7 @@ package grpcwrap import ( "context" + "fmt" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/function" @@ -23,14 +24,16 @@ import ( // implementation. func Provider(p providers.Interface) tfplugin5.ProviderServer { return &provider{ - provider: p, - schema: p.GetProviderSchema(), + provider: p, + schema: p.GetProviderSchema(), + identitySchemas: p.GetResourceIdentitySchemas(), } } type provider struct { - provider providers.Interface - schema providers.GetProviderSchemaResponse + provider providers.Interface + schema providers.GetProviderSchemaResponse + identitySchemas providers.GetResourceIdentitySchemasResponse } func (p *provider) GetMetadata(_ context.Context, req *tfplugin5.GetMetadata_Request) (*tfplugin5.GetMetadata_Response, error) { @@ -66,7 +69,7 @@ func (p *provider) GetSchema(_ context.Context, req *tfplugin5.GetProviderSchema } for typ, dat := range p.schema.DataSources { resp.DataSourceSchemas[typ] = &tfplugin5.Schema{ - Version: dat.Version, + Version: int64(dat.Version), Block: convert.ConfigSchemaToProto(dat.Body), } } @@ -540,11 +543,45 @@ func (p *provider) CallFunction(_ context.Context, req *tfplugin5.CallFunction_R } func (p *provider) GetResourceIdentitySchemas(_ context.Context, req *tfplugin5.GetResourceIdentitySchemas_Request) (*tfplugin5.GetResourceIdentitySchemas_Response, error) { - panic("Not implemented yet") + resp := &tfplugin5.GetResourceIdentitySchemas_Response{ + IdentitySchemas: map[string]*tfplugin5.ResourceIdentitySchema{}, + Diagnostics: []*tfplugin5.Diagnostic{}, + } + + for name, schema := range p.identitySchemas.IdentityTypes { + resp.IdentitySchemas[name] = convert.ResourceIdentitySchemaToProto(schema) + } + + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, p.identitySchemas.Diagnostics) + return resp, nil } func (p *provider) UpgradeResourceIdentity(_ context.Context, req *tfplugin5.UpgradeResourceIdentity_Request) (*tfplugin5.UpgradeResourceIdentity_Response, error) { - panic("Not implemented yet") + resp := &tfplugin5.UpgradeResourceIdentity_Response{} + resource, ok := p.identitySchemas.IdentityTypes[req.TypeName] + if !ok { + return nil, fmt.Errorf("resource identity schema not found for type %q", req.TypeName) + } + ty := resource.Body.ImpliedType() + upgradeResp := p.provider.UpgradeResourceIdentity(providers.UpgradeResourceIdentityRequest{ + TypeName: req.TypeName, + Version: req.Version, + RawIdentityJSON: req.RawIdentity.Json, + }) + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, upgradeResp.Diagnostics) + if upgradeResp.Diagnostics.HasErrors() { + return resp, nil + } + + dv, err := encodeDynamicValue(upgradeResp.UpgradedIdentity, ty) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + resp.UpgradedIdentity = &tfplugin5.ResourceIdentityData{ + IdentityData: dv, + } + return resp, nil } func (p *provider) Stop(context.Context, *tfplugin5.Stop_Request) (*tfplugin5.Stop_Response, error) { diff --git a/internal/grpcwrap/provider6.go b/internal/grpcwrap/provider6.go index 4fc36f5cbf55..185a9f23fec1 100644 --- a/internal/grpcwrap/provider6.go +++ b/internal/grpcwrap/provider6.go @@ -5,6 +5,7 @@ package grpcwrap import ( "context" + "fmt" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/function" @@ -24,14 +25,16 @@ import ( // internal provider implementation. func Provider6(p providers.Interface) tfplugin6.ProviderServer { return &provider6{ - provider: p, - schema: p.GetProviderSchema(), + provider: p, + schema: p.GetProviderSchema(), + identitySchemas: p.GetResourceIdentitySchemas(), } } type provider6 struct { - provider providers.Interface - schema providers.GetProviderSchemaResponse + provider providers.Interface + schema providers.GetProviderSchemaResponse + identitySchemas providers.GetResourceIdentitySchemasResponse } func (p *provider6) GetMetadata(_ context.Context, req *tfplugin6.GetMetadata_Request) (*tfplugin6.GetMetadata_Response, error) { @@ -68,13 +71,13 @@ func (p *provider6) GetProviderSchema(_ context.Context, req *tfplugin6.GetProvi } for typ, dat := range p.schema.DataSources { resp.DataSourceSchemas[typ] = &tfplugin6.Schema{ - Version: dat.Version, + Version: int64(dat.Version), Block: convert.ConfigSchemaToProto(dat.Body), } } for typ, dat := range p.schema.EphemeralResourceTypes { resp.EphemeralResourceSchemas[typ] = &tfplugin6.Schema{ - Version: dat.Version, + Version: int64(dat.Version), Block: convert.ConfigSchemaToProto(dat.Body), } } @@ -590,11 +593,48 @@ func (p *provider6) CallFunction(_ context.Context, req *tfplugin6.CallFunction_ } func (p *provider6) GetResourceIdentitySchemas(_ context.Context, req *tfplugin6.GetResourceIdentitySchemas_Request) (*tfplugin6.GetResourceIdentitySchemas_Response, error) { - panic("Not implemented yet") + resp := &tfplugin6.GetResourceIdentitySchemas_Response{ + IdentitySchemas: map[string]*tfplugin6.ResourceIdentitySchema{}, + Diagnostics: []*tfplugin6.Diagnostic{}, + } + + for name, schema := range p.identitySchemas.IdentityTypes { + resp.IdentitySchemas[name] = convert.ResourceIdentitySchemaToProto(schema) + } + + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, p.identitySchemas.Diagnostics) + return resp, nil } func (p *provider6) UpgradeResourceIdentity(_ context.Context, req *tfplugin6.UpgradeResourceIdentity_Request) (*tfplugin6.UpgradeResourceIdentity_Response, error) { - panic("Not implemented yet") + resp := &tfplugin6.UpgradeResourceIdentity_Response{} + resource, ok := p.identitySchemas.IdentityTypes[req.TypeName] + if !ok { + return nil, fmt.Errorf("resource identity schema not found for type %q", req.TypeName) + } + ty := resource.Body.ImpliedType() + + upgradeResp := p.provider.UpgradeResourceIdentity(providers.UpgradeResourceIdentityRequest{ + TypeName: req.TypeName, + Version: req.Version, + RawIdentityJSON: req.RawIdentity.Json, + }) + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, upgradeResp.Diagnostics) + + if upgradeResp.Diagnostics.HasErrors() { + return resp, nil + } + + dv, err := encodeDynamicValue6(upgradeResp.UpgradedIdentity, ty) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + resp.UpgradedIdentity = &tfplugin6.ResourceIdentityData{ + IdentityData: dv, + } + return resp, nil } func (p *provider6) StopProvider(context.Context, *tfplugin6.StopProvider_Request) (*tfplugin6.StopProvider_Response, error) { diff --git a/internal/lang/globalref/analyzer_meta_references.go b/internal/lang/globalref/analyzer_meta_references.go index c00e2d532304..b81a1a1755b1 100644 --- a/internal/lang/globalref/analyzer_meta_references.go +++ b/internal/lang/globalref/analyzer_meta_references.go @@ -205,8 +205,8 @@ func (a *Analyzer) metaReferencesResourceInstance(moduleAddr addrs.ModuleInstanc return nil } - resourceTypeSchema, _ := providerSchema.SchemaForResourceAddr(addr.Resource) - if resourceTypeSchema == nil { + resourceTypeSchema := providerSchema.SchemaForResourceAddr(addr.Resource) + if resourceTypeSchema.Body == nil { return nil } @@ -234,11 +234,11 @@ func (a *Analyzer) metaReferencesResourceInstance(moduleAddr addrs.ModuleInstanc // Caller must also update "schema" if necessary. } traverseInBlock := func(name string) ([]hcl.Body, []hcl.Expression) { - if attr := schema.Attributes[name]; attr != nil { + if attr := schema.Body.Attributes[name]; attr != nil { // When we reach a specific attribute we can't traverse any deeper, because attributes are the leaves of the schema. - schema = nil + schema.Body = nil return traverseAttr(bodies, name) - } else if blockType := schema.BlockTypes[name]; blockType != nil { + } else if blockType := schema.Body.BlockTypes[name]; blockType != nil { // We need to take a different action here depending on // the nesting mode of the block type. Some require us // to traverse in two steps in order to select a specific @@ -248,7 +248,7 @@ func (a *Analyzer) metaReferencesResourceInstance(moduleAddr addrs.ModuleInstanc case configschema.NestingSingle, configschema.NestingGroup: // There should be only zero or one blocks of this // type, so we can traverse in only one step. - schema = &blockType.Block + schema.Body = &blockType.Block return traverseNestedBlockSingle(bodies, name) case configschema.NestingMap, configschema.NestingList, configschema.NestingSet: steppingThrough = blockType @@ -258,14 +258,14 @@ func (a *Analyzer) metaReferencesResourceInstance(moduleAddr addrs.ModuleInstanc // we add something new in future we'll bail out // here and conservatively return everything under // the current traversal point. - schema = nil + schema.Body = nil return nil, nil } } // We'll get here if the given name isn't in the schema at all. If so, // there's nothing else to be done here. - schema = nil + schema.Body = nil return nil, nil } Steps: @@ -281,7 +281,7 @@ Steps: // a specific attribute) and so we'll stop early, assuming that // any remaining steps are traversals into an attribute expression // result. - if schema == nil { + if schema.Body == nil { break } @@ -299,10 +299,10 @@ Steps: continue } nextStep(traverseNestedBlockMap(bodies, steppingThroughType, step.Name)) - schema = &steppingThrough.Block + schema.Body = &steppingThrough.Block default: nextStep(traverseInBlock(step.Name)) - if schema == nil { + if schema.Body == nil { // traverseInBlock determined that we've traversed as // deep as we can with reference to schema, so we'll // stop here and just process whatever's selected. @@ -320,7 +320,7 @@ Steps: continue } nextStep(traverseNestedBlockMap(bodies, steppingThroughType, keyVal.AsString())) - schema = &steppingThrough.Block + schema.Body = &steppingThrough.Block case configschema.NestingList: idxVal, err := convert.Convert(step.Key, cty.Number) if err != nil { // Invalid traversal, so can't have any refs @@ -334,7 +334,7 @@ Steps: continue } nextStep(traverseNestedBlockList(bodies, steppingThroughType, idx)) - schema = &steppingThrough.Block + schema.Body = &steppingThrough.Block default: // Note that NestingSet ends up in here because we don't // actually allow traversing into set-backed block types, @@ -352,7 +352,7 @@ Steps: continue } nextStep(traverseInBlock(nameVal.AsString())) - if schema == nil { + if schema.Body == nil { // traverseInBlock determined that we've traversed as // deep as we can with reference to schema, so we'll // stop here and just process whatever's selected. @@ -392,9 +392,9 @@ Steps: moreRefs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, expr) refs = append(refs, moreRefs...) } - if schema != nil { + if schema.Body != nil { for _, body := range bodies { - moreRefs, _ := langrefs.ReferencesInBlock(addrs.ParseRef, body, schema) + moreRefs, _ := langrefs.ReferencesInBlock(addrs.ParseRef, body, schema.Body) refs = append(refs, moreRefs...) } } diff --git a/internal/legacy/go.sum b/internal/legacy/go.sum index 1ae4bc0c16e1..2aa7150dbca2 100644 --- a/internal/legacy/go.sum +++ b/internal/legacy/go.sum @@ -17,20 +17,14 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go v0.110.7 h1:rJyC7nWRg2jWGZ4wSJ5nY65GTdYJkg0cd/uXb+ACI6o= -cloud.google.com/go v0.110.7/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= -cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/iam v1.1.1 h1:lW7fzj15aVIXYHREOqjRBV9PsH0Z6u8Y46a1YGvQP4Y= -cloud.google.com/go/iam v1.1.1/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -41,8 +35,6 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= -cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= @@ -52,10 +44,6 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/apparentlymart/go-versions v1.0.2 h1:n5Gg9YvSLK8Zzpy743J7abh2jt7z7ammOQ0oTd/5oA4= github.com/apparentlymart/go-versions v1.0.2/go.mod h1:YF5j7IQtrOAOnsGkniupEA5bfCjzd7i14yu0shZavyM= -github.com/aws/aws-sdk-go v1.44.122 h1:p6mw01WBaNpbdP2xrisz5tIkcNwzj/HysobNoaAHjgo= -github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= -github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= -github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= @@ -85,8 +73,6 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -108,8 +94,6 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -137,28 +121,12 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= -github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= -github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4= -github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= -github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-getter v1.7.8 h1:mshVHx1Fto0/MydBekWan5zUipGq7jO0novchgMmSiY= -github.com/hashicorp/go-getter v1.7.8/go.mod h1:2c6CboOEb9jG6YvmC9xdD+tyAFsrUaJPedwXDGr0TM4= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= -github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= -github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= -github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= github.com/hashicorp/go-slug v0.16.3 h1:pe0PMwz2UWN1168QksdW/d7u057itB2gY568iF0E2Ns= github.com/hashicorp/go-slug v0.16.3/go.mod h1:THWVTAXwJEinbsp4/bBRcmbaO5EYNLTqxbG4tZ3gCYQ= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= @@ -173,13 +141,9 @@ github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= -github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -192,10 +156,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= -github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -213,8 +173,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= -github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -229,8 +187,6 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -238,8 +194,6 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= -golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -318,8 +272,6 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -436,8 +388,6 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -457,8 +407,6 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513 google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.126.0 h1:q4GJq+cAdMAC7XP7njvQ4tvohGLiSlytuL4BQxbIZ+o= -google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -466,8 +414,6 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -504,12 +450,6 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY= -google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= -google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q= -google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -526,8 +466,6 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= -google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -538,8 +476,6 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= diff --git a/internal/plugin/convert/diagnostics_test.go b/internal/plugin/convert/diagnostics_test.go index c39e2002034d..2c4701c5874f 100644 --- a/internal/plugin/convert/diagnostics_test.go +++ b/internal/plugin/convert/diagnostics_test.go @@ -21,6 +21,8 @@ var ignoreUnexported = cmpopts.IgnoreUnexported( proto.Schema_Block{}, proto.Schema_NestedBlock{}, proto.Schema_Attribute{}, + proto.ResourceIdentitySchema{}, + proto.ResourceIdentitySchema_IdentityAttribute{}, ) func TestProtoDiagnostics(t *testing.T) { diff --git a/internal/plugin/convert/schema.go b/internal/plugin/convert/schema.go index 8c181b0fd501..ddd4d69e2ccb 100644 --- a/internal/plugin/convert/schema.go +++ b/internal/plugin/convert/schema.go @@ -90,11 +90,19 @@ func protoSchemaNestedBlock(name string, b *configschema.NestedBlock) *proto.Sch } // ProtoToProviderSchema takes a proto.Schema and converts it to a providers.Schema. -func ProtoToProviderSchema(s *proto.Schema) providers.Schema { - return providers.Schema{ +// It takes an optional resource identity schema for resources that support identity. +func ProtoToProviderSchema(s *proto.Schema, id *proto.ResourceIdentitySchema) providers.Schema { + schema := providers.Schema{ Version: s.Version, Body: ProtoToConfigSchema(s.Block), } + + if id != nil { + schema.IdentityVersion = id.Version + schema.Identity = ProtoToIdentitySchema(id.IdentityAttributes) + } + + return schema } // ProtoToConfigSchema takes the GetSchcema_Block from a grpc response and converts it @@ -188,3 +196,54 @@ func sortedKeys(m interface{}) []string { sort.Strings(keys) return keys } + +func ProtoToIdentitySchema(attributes []*proto.ResourceIdentitySchema_IdentityAttribute) *configschema.Object { + obj := &configschema.Object{ + Attributes: make(map[string]*configschema.Attribute), + Nesting: configschema.NestingSingle, + } + + for _, a := range attributes { + attr := &configschema.Attribute{ + Description: a.Description, + Required: a.RequiredForImport, + Optional: a.OptionalForImport, + } + + if err := json.Unmarshal(a.Type, &attr.Type); err != nil { + panic(err) + } + + obj.Attributes[a.Name] = attr + } + + return obj +} + +func ResourceIdentitySchemaToProto(b providers.IdentitySchema) *proto.ResourceIdentitySchema { + attrs := []*proto.ResourceIdentitySchema_IdentityAttribute{} + for _, name := range sortedKeys(b.Body.Attributes) { + a := b.Body.Attributes[name] + + attr := &proto.ResourceIdentitySchema_IdentityAttribute{ + Name: name, + Description: a.Description, + RequiredForImport: a.Required, + OptionalForImport: a.Optional, + } + + ty, err := json.Marshal(a.Type) + if err != nil { + panic(err) + } + + attr.Type = ty + + attrs = append(attrs, attr) + } + + return &proto.ResourceIdentitySchema{ + Version: b.Version, + IdentityAttributes: attrs, + } +} diff --git a/internal/plugin/convert/schema_test.go b/internal/plugin/convert/schema_test.go index 204494e424a1..5c66e077e1e1 100644 --- a/internal/plugin/convert/schema_test.go +++ b/internal/plugin/convert/schema_test.go @@ -9,6 +9,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/providers" proto "github.com/hashicorp/terraform/internal/tfplugin5" "github.com/zclconf/go-cty/cty" ) @@ -362,3 +363,90 @@ func TestConvertProtoSchemaBlocks(t *testing.T) { }) } } + +func TestProtoToResourceIdentitySchema(t *testing.T) { + tests := map[string]struct { + Attributes []*proto.ResourceIdentitySchema_IdentityAttribute + Want *configschema.Object + }{ + "simple": { + []*proto.ResourceIdentitySchema_IdentityAttribute{ + { + Name: "id", + Type: []byte(`"string"`), + RequiredForImport: true, + OptionalForImport: false, + Description: "Something", + }, + }, + &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Description: "Something", + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + converted := ProtoToIdentitySchema(tc.Attributes) + if !cmp.Equal(converted, tc.Want, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(converted, tc.Want, typeComparer, valueComparer, equateEmpty)) + } + }) + } +} + +func TestResourceIdentitySchemaToProto(t *testing.T) { + tests := map[string]struct { + Want *proto.ResourceIdentitySchema + Schema providers.IdentitySchema + }{ + "attributes": { + &proto.ResourceIdentitySchema{ + Version: 1, + IdentityAttributes: []*proto.ResourceIdentitySchema_IdentityAttribute{ + { + Name: "optional", + Type: []byte(`"string"`), + OptionalForImport: true, + }, + { + Name: "required", + Type: []byte(`"number"`), + RequiredForImport: true, + }, + }, + }, + providers.IdentitySchema{ + Version: 1, + Body: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "optional": { + Type: cty.String, + Optional: true, + }, + "required": { + Type: cty.Number, + Required: true, + }, + }, + }, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + converted := ResourceIdentitySchemaToProto(tc.Schema) + if !cmp.Equal(converted, tc.Want, typeComparer, equateEmpty, ignoreUnexported) { + t.Fatal(cmp.Diff(converted, tc.Want, typeComparer, equateEmpty, ignoreUnexported)) + } + }) + } +} diff --git a/internal/plugin/grpc_provider.go b/internal/plugin/grpc_provider.go index b36aefa445d6..ed916ea3d89d 100644 --- a/internal/plugin/grpc_provider.go +++ b/internal/plugin/grpc_provider.go @@ -16,6 +16,8 @@ import ( ctyjson "github.com/zclconf/go-cty/cty/json" "github.com/zclconf/go-cty/cty/msgpack" "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/logging" @@ -81,9 +83,6 @@ func (p *GRPCProvider) GetProviderSchema() providers.GetProviderSchemaResponse { defer p.mu.Unlock() // check the global cache if we can - // FIXME: A global cache is inappropriate when Terraform Core is being - // used in a non-Terraform-CLI mode where we shouldn't assume that all - // calls share the same provider implementations. if !p.Addr.IsZero() { if resp, ok := providers.SchemaCache.Get(p.Addr); ok && resp.ServerCapabilities.GetProviderSchemaOptional { logger.Trace("GRPCProvider: returning cached schema", p.Addr.String()) @@ -129,23 +128,38 @@ func (p *GRPCProvider) GetProviderSchema() providers.GetProviderSchemaResponse { return resp } - resp.Provider = convert.ProtoToProviderSchema(protoResp.Provider) + identResp, err := p.client.GetResourceIdentitySchemas(p.ctx, new(proto.GetResourceIdentitySchemas_Request)) + if err != nil { + if status.Code(err) == codes.Unimplemented { + // We don't treat this as an error if older providers don't implement this method, + // so we create an empty map for identity schemas + identResp = &proto.GetResourceIdentitySchemas_Response{ + IdentitySchemas: map[string]*proto.ResourceIdentitySchema{}, + } + } else { + resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) + return resp + } + } + + resp.Provider = convert.ProtoToProviderSchema(protoResp.Provider, nil) if protoResp.ProviderMeta == nil { logger.Debug("No provider meta schema returned") } else { - resp.ProviderMeta = convert.ProtoToProviderSchema(protoResp.ProviderMeta) + resp.ProviderMeta = convert.ProtoToProviderSchema(protoResp.ProviderMeta, nil) } for name, res := range protoResp.ResourceSchemas { - resp.ResourceTypes[name] = convert.ProtoToProviderSchema(res) + id := identResp.IdentitySchemas[name] // We're fine if the id is not found + resp.ResourceTypes[name] = convert.ProtoToProviderSchema(res, id) } for name, data := range protoResp.DataSourceSchemas { - resp.DataSources[name] = convert.ProtoToProviderSchema(data) + resp.DataSources[name] = convert.ProtoToProviderSchema(data, nil) } for name, ephem := range protoResp.EphemeralResourceSchemas { - resp.EphemeralResourceTypes[name] = convert.ProtoToProviderSchema(ephem) + resp.EphemeralResourceTypes[name] = convert.ProtoToProviderSchema(ephem, nil) } if decls, err := convert.FunctionDeclsFromProto(protoResp.Functions); err == nil { @@ -162,9 +176,6 @@ func (p *GRPCProvider) GetProviderSchema() providers.GetProviderSchemaResponse { } // set the global cache if we can - // FIXME: A global cache is inappropriate when Terraform Core is being - // used in a non-Terraform-CLI mode where we shouldn't assume that all - // calls share the same provider implementations. if !p.Addr.IsZero() { providers.SchemaCache.Set(p.Addr, resp) } @@ -176,6 +187,38 @@ func (p *GRPCProvider) GetProviderSchema() providers.GetProviderSchemaResponse { return resp } +func (p *GRPCProvider) GetResourceIdentitySchemas() providers.GetResourceIdentitySchemasResponse { + var resp providers.GetResourceIdentitySchemasResponse + + resp.IdentityTypes = make(map[string]providers.IdentitySchema) + + protoResp, err := p.client.GetResourceIdentitySchemas(p.ctx, new(proto.GetResourceIdentitySchemas_Request)) + if err != nil { + if status.Code(err) == codes.Unimplemented { + // We expect no error here if older providers don't implement this method + return resp + } + + resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) + return resp + } + + resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) + + if resp.Diagnostics.HasErrors() { + return resp + } + + for name, res := range protoResp.IdentitySchemas { + resp.IdentityTypes[name] = providers.IdentitySchema{ + Version: res.Version, + Body: convert.ProtoToIdentitySchema(res.IdentityAttributes), + } + } + + return resp +} + func (p *GRPCProvider) ValidateProviderConfig(r providers.ValidateProviderConfigRequest) (resp providers.ValidateProviderConfigResponse) { logger.Trace("GRPCProvider: ValidateProviderConfig") @@ -333,6 +376,52 @@ func (p *GRPCProvider) UpgradeResourceState(r providers.UpgradeResourceStateRequ return resp } +func (p *GRPCProvider) UpgradeResourceIdentity(r providers.UpgradeResourceIdentityRequest) (resp providers.UpgradeResourceIdentityResponse) { + logger.Trace("GRPCProvider: UpgradeResourceIdentity") + + 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 identity type %q", r.TypeName)) + return resp + } + + protoReq := &proto.UpgradeResourceIdentity_Request{ + TypeName: r.TypeName, + Version: int64(r.Version), + RawIdentity: &proto.RawState{ + Json: r.RawIdentityJSON, + }, + } + + protoResp, err := p.client.UpgradeResourceIdentity(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.Identity.ImpliedType() + resp.UpgradedIdentity = cty.NullVal(ty) + if protoResp.UpgradedIdentity == nil { + return resp + } + + identity, err := decodeDynamicValue(protoResp.UpgradedIdentity.IdentityData, ty) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + resp.UpgradedIdentity = identity + + return resp +} + func (p *GRPCProvider) ConfigureProvider(r providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { logger.Trace("GRPCProvider: ConfigureProvider") diff --git a/internal/plugin/grpc_provider_test.go b/internal/plugin/grpc_provider_test.go index eb884c6fe6f0..8e3e25a04826 100644 --- a/internal/plugin/grpc_provider_test.go +++ b/internal/plugin/grpc_provider_test.go @@ -16,6 +16,8 @@ import ( "github.com/hashicorp/terraform/internal/tfdiags" "github.com/zclconf/go-cty/cty" "go.uber.org/mock/gomock" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/timestamppb" mockproto "github.com/hashicorp/terraform/internal/plugin/mock_proto" @@ -35,6 +37,13 @@ func mockProviderClient(t *testing.T) *mockproto.MockProviderClient { gomock.Any(), ).Return(providerProtoSchema(), nil) + // GetResourceIdentitySchemas is called as part of GetSchema + client.EXPECT().GetResourceIdentitySchemas( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(providerResourceIdentitySchemas(), nil) + return client } @@ -114,6 +123,23 @@ func providerProtoSchema() *proto.GetProviderSchema_Response { } } +func providerResourceIdentitySchemas() *proto.GetResourceIdentitySchemas_Response { + return &proto.GetResourceIdentitySchemas_Response{ + IdentitySchemas: map[string]*proto.ResourceIdentitySchema{ + "resource": { + Version: 1, + IdentityAttributes: []*proto.ResourceIdentitySchema_IdentityAttribute{ + { + Name: "attr", + Type: []byte(`"string"`), + RequiredForImport: true, + }, + }, + }, + }, + } +} + func TestGRPCProvider_GetSchema(t *testing.T) { p := &GRPCProvider{ client: mockProviderClient(t), @@ -196,6 +222,94 @@ func TestGRPCProvider_GetSchema_ResponseErrorDiagnostic(t *testing.T) { checkDiagsHasError(t, resp.Diagnostics) } +func TestGRPCProvider_GetSchema_IdentityError(t *testing.T) { + ctrl := gomock.NewController(t) + client := mockproto.NewMockProviderClient(ctrl) + + client.EXPECT().GetSchema( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(providerProtoSchema(), nil) + + client.EXPECT().GetResourceIdentitySchemas( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(&proto.GetResourceIdentitySchemas_Response{}, fmt.Errorf("test error")) + + p := &GRPCProvider{ + client: client, + } + + resp := p.GetProviderSchema() + + checkDiagsHasError(t, resp.Diagnostics) +} + +func TestGRPCProvider_GetSchema_IdentityUnimplemented(t *testing.T) { + ctrl := gomock.NewController(t) + client := mockproto.NewMockProviderClient(ctrl) + + client.EXPECT().GetSchema( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(providerProtoSchema(), nil) + + client.EXPECT().GetResourceIdentitySchemas( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(&proto.GetResourceIdentitySchemas_Response{}, status.Error(codes.Unimplemented, "test error")) + + p := &GRPCProvider{ + client: client, + } + + resp := p.GetProviderSchema() + + checkDiags(t, resp.Diagnostics) +} + +func TestGRPCProvider_GetResourceIdentitySchemas(t *testing.T) { + ctrl := gomock.NewController(t) + client := mockproto.NewMockProviderClient(ctrl) + + client.EXPECT().GetResourceIdentitySchemas( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(providerResourceIdentitySchemas(), nil) + + p := &GRPCProvider{ + client: client, + } + + resp := p.GetResourceIdentitySchemas() + + checkDiags(t, resp.Diagnostics) +} + +func TestGRPCProvider_GetResourceIdentitySchemas_Unimplemented(t *testing.T) { + ctrl := gomock.NewController(t) + client := mockproto.NewMockProviderClient(ctrl) + + client.EXPECT().GetResourceIdentitySchemas( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(&proto.GetResourceIdentitySchemas_Response{}, status.Error(codes.Unimplemented, "test error")) + + p := &GRPCProvider{ + client: client, + } + + resp := p.GetResourceIdentitySchemas() + + checkDiags(t, resp.Diagnostics) +} + func TestGRPCProvider_PrepareProviderConfig(t *testing.T) { client := mockProviderClient(t) p := &GRPCProvider{ @@ -312,6 +426,84 @@ func TestGRPCProvider_UpgradeResourceStateJSON(t *testing.T) { } } +func TestGRPCProvider_UpgradeResourceIdentity(t *testing.T) { + testCases := []struct { + desc string + response *proto.UpgradeResourceIdentity_Response + expectError bool + expectedValue cty.Value + }{ + { + "successful upgrade", + &proto.UpgradeResourceIdentity_Response{ + UpgradedIdentity: &proto.ResourceIdentityData{ + IdentityData: &proto.DynamicValue{ + Json: []byte(`{"attr":"bar"}`), + }, + }, + }, + false, + cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("bar")}), + }, + { + "response with error diagnostic", + &proto.UpgradeResourceIdentity_Response{ + Diagnostics: []*proto.Diagnostic{ + { + Severity: proto.Diagnostic_ERROR, + Summary: "test error", + Detail: "test error detail", + }, + }, + }, + true, + cty.NilVal, + }, + { + "schema mismatch", + &proto.UpgradeResourceIdentity_Response{ + UpgradedIdentity: &proto.ResourceIdentityData{ + IdentityData: &proto.DynamicValue{ + Json: []byte(`{"attr_new":"bar"}`), + }, + }, + }, + true, + cty.NilVal, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + client.EXPECT().UpgradeResourceIdentity( + gomock.Any(), + gomock.Any(), + ).Return(tc.response, nil) + + resp := p.UpgradeResourceIdentity(providers.UpgradeResourceIdentityRequest{ + TypeName: "resource", + Version: 0, + RawIdentityJSON: []byte(`{"old_attr":"bar"}`), + }) + + if tc.expectError { + checkDiagsHasError(t, resp.Diagnostics) + } else { + checkDiags(t, resp.Diagnostics) + + if !cmp.Equal(tc.expectedValue, resp.UpgradedIdentity, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(tc.expectedValue, resp.UpgradedIdentity, typeComparer, valueComparer, equateEmpty)) + } + } + }) + } +} + func TestGRPCProvider_Configure(t *testing.T) { client := mockProviderClient(t) p := &GRPCProvider{ diff --git a/internal/plugin6/convert/diagnostics_test.go b/internal/plugin6/convert/diagnostics_test.go index 860849ef8b6e..82cc97d4e8b9 100644 --- a/internal/plugin6/convert/diagnostics_test.go +++ b/internal/plugin6/convert/diagnostics_test.go @@ -20,6 +20,8 @@ var ignoreUnexported = cmpopts.IgnoreUnexported( proto.Schema_NestedBlock{}, proto.Schema_Attribute{}, proto.Schema_Object{}, + proto.ResourceIdentitySchema{}, + proto.ResourceIdentitySchema_IdentityAttribute{}, ) func TestProtoDiagnostics(t *testing.T) { diff --git a/internal/plugin6/convert/schema.go b/internal/plugin6/convert/schema.go index ea0b03837fac..f1f47461d553 100644 --- a/internal/plugin6/convert/schema.go +++ b/internal/plugin6/convert/schema.go @@ -96,11 +96,44 @@ func protoSchemaNestedBlock(name string, b *configschema.NestedBlock) *proto.Sch } // ProtoToProviderSchema takes a proto.Schema and converts it to a providers.Schema. -func ProtoToProviderSchema(s *proto.Schema) providers.Schema { - return providers.Schema{ +// It takes an optional resource identity schema for resources that support identity. +func ProtoToProviderSchema(s *proto.Schema, id *proto.ResourceIdentitySchema) providers.Schema { + schema := providers.Schema{ Version: s.Version, Body: ProtoToConfigSchema(s.Block), } + + if id != nil { + schema.IdentityVersion = id.Version + schema.Identity = ProtoToIdentitySchema(id.IdentityAttributes) + } + + return schema +} + +func ProtoToIdentitySchema(attributes []*proto.ResourceIdentitySchema_IdentityAttribute) *configschema.Object { + obj := &configschema.Object{ + Attributes: make(map[string]*configschema.Attribute), + Nesting: configschema.NestingSingle, + } + + for _, a := range attributes { + attr := &configschema.Attribute{ + Description: a.Description, + Required: a.RequiredForImport, + Optional: a.OptionalForImport, + } + + if a.Type != nil { + if err := json.Unmarshal(a.Type, &attr.Type); err != nil { + panic(err) + } + } + + obj.Attributes[a.Name] = attr + } + + return obj } // ProtoToConfigSchema takes the GetSchcema_Block from a grpc response and converts it @@ -301,3 +334,32 @@ func configschemaObjectToProto(b *configschema.Object) *proto.Schema_Object { Nesting: nesting, } } + +func ResourceIdentitySchemaToProto(schema providers.IdentitySchema) *proto.ResourceIdentitySchema { + identityAttributes := []*proto.ResourceIdentitySchema_IdentityAttribute{} + + for _, name := range sortedKeys(schema.Body.Attributes) { + a := schema.Body.Attributes[name] + attr := &proto.ResourceIdentitySchema_IdentityAttribute{ + Name: name, + Description: a.Description, + RequiredForImport: a.Required, + OptionalForImport: a.Optional, + } + + if a.Type != cty.NilType { + ty, err := json.Marshal(a.Type) + if err != nil { + panic(err) + } + attr.Type = ty + } + + identityAttributes = append(identityAttributes, attr) + } + + return &proto.ResourceIdentitySchema{ + Version: schema.Version, + IdentityAttributes: identityAttributes, + } +} diff --git a/internal/plugin6/convert/schema_test.go b/internal/plugin6/convert/schema_test.go index a50e0d34e871..d7b8caba93cb 100644 --- a/internal/plugin6/convert/schema_test.go +++ b/internal/plugin6/convert/schema_test.go @@ -9,6 +9,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/providers" proto "github.com/hashicorp/terraform/internal/tfplugin6" "github.com/zclconf/go-cty/cty" ) @@ -624,3 +625,90 @@ func TestConvertProtoSchemaBlocks(t *testing.T) { }) } } + +func TestProtoToResourceIdentitySchema(t *testing.T) { + tests := map[string]struct { + Attributes []*proto.ResourceIdentitySchema_IdentityAttribute + Want *configschema.Object + }{ + "simple": { + []*proto.ResourceIdentitySchema_IdentityAttribute{ + { + Name: "id", + Type: []byte(`"string"`), + RequiredForImport: true, + OptionalForImport: false, + Description: "Something", + }, + }, + &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Description: "Something", + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + converted := ProtoToIdentitySchema(tc.Attributes) + if !cmp.Equal(converted, tc.Want, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(converted, tc.Want, typeComparer, valueComparer, equateEmpty)) + } + }) + } +} + +func TestResourceIdentitySchemaToProto(t *testing.T) { + tests := map[string]struct { + Want *proto.ResourceIdentitySchema + Schema providers.IdentitySchema + }{ + "attributes": { + &proto.ResourceIdentitySchema{ + Version: 1, + IdentityAttributes: []*proto.ResourceIdentitySchema_IdentityAttribute{ + { + Name: "optional", + Type: []byte(`"string"`), + OptionalForImport: true, + }, + { + Name: "required", + Type: []byte(`"number"`), + RequiredForImport: true, + }, + }, + }, + providers.IdentitySchema{ + Version: 1, + Body: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "optional": { + Type: cty.String, + Optional: true, + }, + "required": { + Type: cty.Number, + Required: true, + }, + }, + }, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + converted := ResourceIdentitySchemaToProto(tc.Schema) + if !cmp.Equal(converted, tc.Want, typeComparer, equateEmpty, ignoreUnexported) { + t.Fatal(cmp.Diff(converted, tc.Want, typeComparer, equateEmpty, ignoreUnexported)) + } + }) + } +} diff --git a/internal/plugin6/grpc_provider.go b/internal/plugin6/grpc_provider.go index 2464cce6e7b0..d4495f7851e2 100644 --- a/internal/plugin6/grpc_provider.go +++ b/internal/plugin6/grpc_provider.go @@ -16,6 +16,8 @@ import ( ctyjson "github.com/zclconf/go-cty/cty/json" "github.com/zclconf/go-cty/cty/msgpack" "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/logging" @@ -80,9 +82,6 @@ func (p *GRPCProvider) GetProviderSchema() providers.GetProviderSchemaResponse { defer p.mu.Unlock() // check the global cache if we can - // FIXME: A global cache is inappropriate when Terraform Core is being - // used in a non-Terraform-CLI mode where we shouldn't assume that all - // calls share the same provider implementations. if !p.Addr.IsZero() { if resp, ok := providers.SchemaCache.Get(p.Addr); ok && resp.ServerCapabilities.GetProviderSchemaOptional { logger.Trace("GRPCProvider.v6: returning cached schema", p.Addr.String()) @@ -129,23 +128,38 @@ func (p *GRPCProvider) GetProviderSchema() providers.GetProviderSchemaResponse { return resp } - resp.Provider = convert.ProtoToProviderSchema(protoResp.Provider) + identResp, err := p.client.GetResourceIdentitySchemas(p.ctx, new(proto6.GetResourceIdentitySchemas_Request)) + if err != nil { + if status.Code(err) == codes.Unimplemented { + // We don't treat this as an error if older providers don't implement this method, + // so we create an empty map for identity schemas + identResp = &proto6.GetResourceIdentitySchemas_Response{ + IdentitySchemas: map[string]*proto6.ResourceIdentitySchema{}, + } + } else { + resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) + return resp + } + } + + resp.Provider = convert.ProtoToProviderSchema(protoResp.Provider, nil) if protoResp.ProviderMeta == nil { logger.Debug("No provider meta schema returned") } else { - resp.ProviderMeta = convert.ProtoToProviderSchema(protoResp.ProviderMeta) + resp.ProviderMeta = convert.ProtoToProviderSchema(protoResp.ProviderMeta, nil) } for name, res := range protoResp.ResourceSchemas { - resp.ResourceTypes[name] = convert.ProtoToProviderSchema(res) + id := identResp.IdentitySchemas[name] // We're fine if the id is not found + resp.ResourceTypes[name] = convert.ProtoToProviderSchema(res, id) } for name, data := range protoResp.DataSourceSchemas { - resp.DataSources[name] = convert.ProtoToProviderSchema(data) + resp.DataSources[name] = convert.ProtoToProviderSchema(data, nil) } for name, ephem := range protoResp.EphemeralResourceSchemas { - resp.EphemeralResourceTypes[name] = convert.ProtoToProviderSchema(ephem) + resp.EphemeralResourceTypes[name] = convert.ProtoToProviderSchema(ephem, nil) } if decls, err := convert.FunctionDeclsFromProto(protoResp.Functions); err == nil { @@ -162,9 +176,6 @@ func (p *GRPCProvider) GetProviderSchema() providers.GetProviderSchemaResponse { } // set the global cache if we can - // FIXME: A global cache is inappropriate when Terraform Core is being - // used in a non-Terraform-CLI mode where we shouldn't assume that all - // calls share the same provider implementations. if !p.Addr.IsZero() { providers.SchemaCache.Set(p.Addr, resp) } @@ -176,6 +187,40 @@ func (p *GRPCProvider) GetProviderSchema() providers.GetProviderSchemaResponse { return resp } +func (p *GRPCProvider) GetResourceIdentitySchemas() providers.GetResourceIdentitySchemasResponse { + logger.Trace("GRPCProvider.v6: GetResourceIdentitySchemas") + + var resp providers.GetResourceIdentitySchemasResponse + + resp.IdentityTypes = make(map[string]providers.IdentitySchema) + + protoResp, err := p.client.GetResourceIdentitySchemas(p.ctx, new(proto6.GetResourceIdentitySchemas_Request)) + if err != nil { + if status.Code(err) == codes.Unimplemented { + // We expect no error here if older providers don't implement this method + return resp + } + + resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) + return resp + } + + resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) + + if resp.Diagnostics.HasErrors() { + return resp + } + + for name, res := range protoResp.IdentitySchemas { + resp.IdentityTypes[name] = providers.IdentitySchema{ + Version: res.Version, + Body: convert.ProtoToIdentitySchema(res.IdentityAttributes), + } + } + + return resp +} + func (p *GRPCProvider) ValidateProviderConfig(r providers.ValidateProviderConfigRequest) (resp providers.ValidateProviderConfigResponse) { logger.Trace("GRPCProvider.v6: ValidateProviderConfig") @@ -326,6 +371,52 @@ func (p *GRPCProvider) UpgradeResourceState(r providers.UpgradeResourceStateRequ return resp } +func (p *GRPCProvider) UpgradeResourceIdentity(r providers.UpgradeResourceIdentityRequest) (resp providers.UpgradeResourceIdentityResponse) { + logger.Trace("GRPCProvider.v6: UpgradeResourceIdentity") + + 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.UpgradeResourceIdentity_Request{ + TypeName: r.TypeName, + Version: int64(r.Version), + RawIdentity: &proto6.RawState{ + Json: r.RawIdentityJSON, + }, + } + + protoResp, err := p.client.UpgradeResourceIdentity(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.Identity.ImpliedType() + resp.UpgradedIdentity = cty.NullVal(ty) + if protoResp.UpgradedIdentity == nil { + return resp + } + + identity, err := decodeDynamicValue(protoResp.UpgradedIdentity.IdentityData, ty) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + resp.UpgradedIdentity = identity + + return resp +} + func (p *GRPCProvider) ConfigureProvider(r providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { logger.Trace("GRPCProvider.v6: ConfigureProvider") diff --git a/internal/plugin6/grpc_provider_test.go b/internal/plugin6/grpc_provider_test.go index de2aab700443..e87fab12d645 100644 --- a/internal/plugin6/grpc_provider_test.go +++ b/internal/plugin6/grpc_provider_test.go @@ -17,6 +17,8 @@ import ( "github.com/hashicorp/terraform/internal/tfdiags" "github.com/zclconf/go-cty/cty" "go.uber.org/mock/gomock" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/timestamppb" mockproto "github.com/hashicorp/terraform/internal/plugin6/mock_proto" @@ -42,6 +44,13 @@ func mockProviderClient(t *testing.T) *mockproto.MockProviderClient { gomock.Any(), ).Return(providerProtoSchema(), nil) + // GetResourceIdentitySchemas is called as part of GetSchema + client.EXPECT().GetResourceIdentitySchemas( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(providerResourceIdentitySchemas(), nil) + return client } @@ -121,6 +130,23 @@ func providerProtoSchema() *proto.GetProviderSchema_Response { } } +func providerResourceIdentitySchemas() *proto.GetResourceIdentitySchemas_Response { + return &proto.GetResourceIdentitySchemas_Response{ + IdentitySchemas: map[string]*proto.ResourceIdentitySchema{ + "resource": { + Version: 1, + IdentityAttributes: []*proto.ResourceIdentitySchema_IdentityAttribute{ + { + Name: "attr", + Type: []byte(`"string"`), + RequiredForImport: true, + }, + }, + }, + }, + } +} + func TestGRPCProvider_GetSchema(t *testing.T) { p := &GRPCProvider{ client: mockProviderClient(t), @@ -203,6 +229,94 @@ func TestGRPCProvider_GetSchema_ResponseErrorDiagnostic(t *testing.T) { checkDiagsHasError(t, resp.Diagnostics) } +func TestGRPCProvider_GetSchema_IdentityError(t *testing.T) { + ctrl := gomock.NewController(t) + client := mockproto.NewMockProviderClient(ctrl) + + client.EXPECT().GetProviderSchema( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(providerProtoSchema(), nil) + + client.EXPECT().GetResourceIdentitySchemas( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(&proto.GetResourceIdentitySchemas_Response{}, fmt.Errorf("test error")) + + p := &GRPCProvider{ + client: client, + } + + resp := p.GetProviderSchema() + + checkDiagsHasError(t, resp.Diagnostics) +} + +func TestGRPCProvider_GetSchema_IdentityUnimplemented(t *testing.T) { + ctrl := gomock.NewController(t) + client := mockproto.NewMockProviderClient(ctrl) + + client.EXPECT().GetProviderSchema( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(providerProtoSchema(), nil) + + client.EXPECT().GetResourceIdentitySchemas( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(&proto.GetResourceIdentitySchemas_Response{}, status.Error(codes.Unimplemented, "test error")) + + p := &GRPCProvider{ + client: client, + } + + resp := p.GetProviderSchema() + + checkDiags(t, resp.Diagnostics) +} + +func TestGRPCProvider_GetResourceIdentitySchemas(t *testing.T) { + ctrl := gomock.NewController(t) + client := mockproto.NewMockProviderClient(ctrl) + + client.EXPECT().GetResourceIdentitySchemas( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(providerResourceIdentitySchemas(), nil) + + p := &GRPCProvider{ + client: client, + } + + resp := p.GetResourceIdentitySchemas() + + checkDiags(t, resp.Diagnostics) +} + +func TestGRPCProvider_GetResourceIdentitySchemas_Unimplemented(t *testing.T) { + ctrl := gomock.NewController(t) + client := mockproto.NewMockProviderClient(ctrl) + + client.EXPECT().GetResourceIdentitySchemas( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(&proto.GetResourceIdentitySchemas_Response{}, status.Error(codes.Unimplemented, "test error")) + + p := &GRPCProvider{ + client: client, + } + + resp := p.GetResourceIdentitySchemas() + + checkDiags(t, resp.Diagnostics) +} + func TestGRPCProvider_PrepareProviderConfig(t *testing.T) { client := mockProviderClient(t) p := &GRPCProvider{ @@ -319,6 +433,84 @@ func TestGRPCProvider_UpgradeResourceStateJSON(t *testing.T) { } } +func TestGRPCProvider_UpgradeResourceIdentity(t *testing.T) { + testCases := []struct { + desc string + response *proto.UpgradeResourceIdentity_Response + expectError bool + expectedValue cty.Value + }{ + { + "successful upgrade", + &proto.UpgradeResourceIdentity_Response{ + UpgradedIdentity: &proto.ResourceIdentityData{ + IdentityData: &proto.DynamicValue{ + Json: []byte(`{"attr":"bar"}`), + }, + }, + }, + false, + cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("bar")}), + }, + { + "response with error diagnostic", + &proto.UpgradeResourceIdentity_Response{ + Diagnostics: []*proto.Diagnostic{ + { + Severity: proto.Diagnostic_ERROR, + Summary: "test error", + Detail: "test error detail", + }, + }, + }, + true, + cty.NilVal, + }, + { + "schema mismatch", + &proto.UpgradeResourceIdentity_Response{ + UpgradedIdentity: &proto.ResourceIdentityData{ + IdentityData: &proto.DynamicValue{ + Json: []byte(`{"attr_new":"bar"}`), + }, + }, + }, + true, + cty.NilVal, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + client.EXPECT().UpgradeResourceIdentity( + gomock.Any(), + gomock.Any(), + ).Return(tc.response, nil) + + resp := p.UpgradeResourceIdentity(providers.UpgradeResourceIdentityRequest{ + TypeName: "resource", + Version: 0, + RawIdentityJSON: []byte(`{"old_attr":"bar"}`), + }) + + if tc.expectError { + checkDiagsHasError(t, resp.Diagnostics) + } else { + checkDiags(t, resp.Diagnostics) + + if !cmp.Equal(tc.expectedValue, resp.UpgradedIdentity, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(tc.expectedValue, resp.UpgradedIdentity, typeComparer, valueComparer, equateEmpty)) + } + } + }) + } +} + func TestGRPCProvider_Configure(t *testing.T) { client := mockProviderClient(t) p := &GRPCProvider{ diff --git a/internal/provider-simple-v6/provider.go b/internal/provider-simple-v6/provider.go index d5c178a448e1..01186a20fdfc 100644 --- a/internal/provider-simple-v6/provider.go +++ b/internal/provider-simple-v6/provider.go @@ -56,7 +56,7 @@ func Provider() providers.Interface { GetProviderSchemaOptional: true, }, Functions: map[string]providers.FunctionDecl{ - "noop": providers.FunctionDecl{ + "noop": { Parameters: []providers.FunctionParam{ { Name: "noop", @@ -80,6 +80,25 @@ func (s simple) GetProviderSchema() providers.GetProviderSchemaResponse { return s.schema } +func (s simple) GetResourceIdentitySchemas() providers.GetResourceIdentitySchemasResponse { + return providers.GetResourceIdentitySchemasResponse{ + IdentityTypes: map[string]providers.IdentitySchema{ + "simple_resource": { + Version: 0, + Body: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + }, + } +} + func (s simple) ValidateProviderConfig(req providers.ValidateProviderConfigRequest) (resp providers.ValidateProviderConfigResponse) { return resp } @@ -100,6 +119,15 @@ func (p simple) UpgradeResourceState(req providers.UpgradeResourceStateRequest) return resp } +func (p simple) UpgradeResourceIdentity(req providers.UpgradeResourceIdentityRequest) (resp providers.UpgradeResourceIdentityResponse) { + schema := p.GetResourceIdentitySchemas().IdentityTypes[req.TypeName].Body + ty := schema.ImpliedType() + val, err := ctyjson.Unmarshal(req.RawIdentityJSON, ty) + resp.Diagnostics = resp.Diagnostics.Append(err) + resp.UpgradedIdentity = val + return resp +} + func (s simple) ConfigureProvider(providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { return resp } diff --git a/internal/provider-simple/provider.go b/internal/provider-simple/provider.go index a7ce91b51659..2b7e4fc892f0 100644 --- a/internal/provider-simple/provider.go +++ b/internal/provider-simple/provider.go @@ -57,6 +57,25 @@ func (s simple) GetProviderSchema() providers.GetProviderSchemaResponse { return s.schema } +func (s simple) GetResourceIdentitySchemas() providers.GetResourceIdentitySchemasResponse { + return providers.GetResourceIdentitySchemasResponse{ + IdentityTypes: map[string]providers.IdentitySchema{ + "simple_resource": { + Version: 0, + Body: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + }, + } +} + func (s simple) ValidateProviderConfig(req providers.ValidateProviderConfigRequest) (resp providers.ValidateProviderConfigResponse) { return resp } @@ -77,6 +96,15 @@ func (p simple) UpgradeResourceState(req providers.UpgradeResourceStateRequest) return resp } +func (p simple) UpgradeResourceIdentity(req providers.UpgradeResourceIdentityRequest) (resp providers.UpgradeResourceIdentityResponse) { + schema := p.GetResourceIdentitySchemas().IdentityTypes[req.TypeName].Body + ty := schema.ImpliedType() + val, err := ctyjson.Unmarshal(req.RawIdentityJSON, ty) + resp.Diagnostics = resp.Diagnostics.Append(err) + resp.UpgradedIdentity = val + return resp +} + func (s simple) ConfigureProvider(providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { return resp } diff --git a/internal/providers/mock.go b/internal/providers/mock.go index b2976030ccc7..324159a161a3 100644 --- a/internal/providers/mock.go +++ b/internal/providers/mock.go @@ -37,7 +37,8 @@ type Mock struct { Provider Interface Data *configs.MockData - schema *GetProviderSchemaResponse + schema *GetProviderSchemaResponse + identitySchema *GetResourceIdentitySchemasResponse } func (m *Mock) GetProviderSchema() GetProviderSchemaResponse { @@ -66,6 +67,16 @@ func (m *Mock) GetProviderSchema() GetProviderSchemaResponse { return *m.schema } +func (m *Mock) GetResourceIdentitySchemas() GetResourceIdentitySchemasResponse { + if m.identitySchema == nil { + // Cache the schema, it's not changing. + schema := m.Provider.GetResourceIdentitySchemas() + + m.identitySchema = &schema + } + return *m.identitySchema +} + func (m *Mock) ValidateProviderConfig(request ValidateProviderConfigRequest) (response ValidateProviderConfigResponse) { // The config for the mocked providers is consistent, and validated when we // parse the HCL directly. So we'll just make no change here. @@ -131,6 +142,40 @@ func (m *Mock) UpgradeResourceState(request UpgradeResourceStateRequest) (respon return response } +func (m *Mock) UpgradeResourceIdentity(request UpgradeResourceIdentityRequest) (response UpgradeResourceIdentityResponse) { + // We can't do this from a mocked provider, so we just return whatever identity + // is in the request back unchanged. + + schema := m.GetProviderSchema() + response.Diagnostics = response.Diagnostics.Append(schema.Diagnostics) + if schema.Diagnostics.HasErrors() { + // We couldn't retrieve the schema for some reason, so the mock + // provider can't really function. + return response + } + + resource, exists := schema.ResourceTypes[request.TypeName] + if !exists { + // This means something has gone wrong much earlier, we should have + // failed a validation somewhere if a resource type doesn't exist. + panic(fmt.Errorf("failed to retrieve identity schema for resource %s", request.TypeName)) + } + + schemaType := resource.Identity.ImpliedType() + value, err := ctyjson.Unmarshal(request.RawIdentityJSON, schemaType) + + if err != nil { + // Generally, we shouldn't get an error here. The mocked providers are + // only used in tests, and we can't use different versions of providers + // within/between tests so the types should always match up. As such, + // we're not gonna return a super detailed error here. + response.Diagnostics = response.Diagnostics.Append(err) + return response + } + response.UpgradedIdentity = value + return response +} + func (m *Mock) ConfigureProvider(request ConfigureProviderRequest) (response ConfigureProviderResponse) { // Do nothing here, we don't have anything to configure within the mocked // providers. We don't want to call the original providers from here as diff --git a/internal/providers/provider.go b/internal/providers/provider.go index e697f04bb1c8..ac5d22b6cd27 100644 --- a/internal/providers/provider.go +++ b/internal/providers/provider.go @@ -7,7 +7,6 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -17,6 +16,11 @@ type Interface interface { // GetSchema returns the complete schema for the provider. GetProviderSchema() GetProviderSchemaResponse + // GetResourceIdentitySchemas returns the identity schemas for all managed resources + // for the provider. Usually you don't need to call this method directly as GetProviderSchema + // will merge the identity schemas into the provider schema. + GetResourceIdentitySchemas() GetResourceIdentitySchemasResponse + // ValidateProviderConfig allows the provider to validate the configuration. // The ValidateProviderConfigResponse.PreparedConfig field is unused. The // final configuration is not stored in the state, and any modifications @@ -41,6 +45,12 @@ type Interface interface { // result is used for any further processing. UpgradeResourceState(UpgradeResourceStateRequest) UpgradeResourceStateResponse + // UpgradeResourceIdentity is called when the state loader encounters an + // instance identity whose schema version is less than the one reported by + // the currently-used version of the corresponding provider, and the upgraded + // result is used for any further processing. + UpgradeResourceIdentity(UpgradeResourceIdentityRequest) UpgradeResourceIdentityResponse + // Configure configures and initialized the provider. ConfigureProvider(ConfigureProviderRequest) ConfigureProviderResponse @@ -98,7 +108,7 @@ type Interface interface { // should only be used when handling a value for that method. The handling of // of schemas in any other context should always use ProviderSchema, so that // the in-memory representation can be more easily changed separately from the -// RCP protocol. +// RPC protocol. type GetProviderSchemaResponse struct { // Provider is the schema for the provider itself. Provider Schema @@ -127,6 +137,25 @@ type GetProviderSchemaResponse struct { ServerCapabilities ServerCapabilities } +// GetResourceIdentitySchemasResponse is the return type for GetResourceIdentitySchemas, +// and should only be used when handling a value for that method. The handling of +// of schemas in any other context should always use ResourceIdentitySchemas, so that +// the in-memory representation can be more easily changed separately from the +// RPC protocol. +type GetResourceIdentitySchemasResponse struct { + // IdentityTypes map the resource type name to that type's identity schema. + IdentityTypes map[string]IdentitySchema + + // Diagnostics contains any warnings or errors from the method call. + Diagnostics tfdiags.Diagnostics +} + +type IdentitySchema struct { + Version int64 + + Body *configschema.Object +} + // Schema pairs a provider or resource schema with that schema's version. // This is used to be able to upgrade the schema in UpgradeResourceState. // @@ -136,6 +165,9 @@ type GetProviderSchemaResponse struct { type Schema struct { Version int64 Body *configschema.Block + + IdentityVersion int64 + Identity *configschema.Object } // ServerCapabilities allows providers to communicate extra information @@ -254,6 +286,26 @@ type UpgradeResourceStateResponse struct { Diagnostics tfdiags.Diagnostics } +type UpgradeResourceIdentityRequest struct { + // TypeName is the name of the resource type being upgraded + TypeName string + + // Version is version of the schema that created the current identity. + Version int64 + + // RawIdentityJSON contains the identity that needs to be + // upgraded to match the current schema version. + RawIdentityJSON []byte +} + +type UpgradeResourceIdentityResponse struct { + // UpgradedState is the newly upgraded resource identity. + UpgradedIdentity cty.Value + + // Diagnostics contains any warnings or errors from the method call. + Diagnostics tfdiags.Diagnostics +} + type ConfigureProviderRequest struct { // Terraform version is the version string from the running instance of // terraform. Providers can use TerraformVersion to verify compatibility, @@ -344,6 +396,10 @@ type ReadResourceResponse struct { // Deferred if present signals that the provider was not able to fully // complete this operation and a susequent run is required. Deferred *Deferred + + // Identity is the object-typed value representing the identity of the remote + // object within Terraform. + Identity cty.Value } type PlanResourceChangeRequest struct { @@ -484,7 +540,7 @@ type ImportResourceStateResponse struct { } // ImportedResource represents an object being imported into Terraform with the -// help of a provider. An ImportedObject is a RemoteObject that has been read +// 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. type ImportedResource struct { // TypeName is the name of the resource type associated with the @@ -542,24 +598,6 @@ type MoveResourceStateResponse struct { Diagnostics tfdiags.Diagnostics } -// AsInstanceObject converts the receiving ImportedObject into a -// ResourceInstanceObject that has status ObjectReady. -// -// The returned object does not know its own resource type, so the caller must -// retain the ResourceType value from the source object if this information is -// needed. -// -// The returned object also has no dependency addresses, but the caller may -// freely modify the direct fields of the returned object without affecting -// the receiver. -func (ir ImportedResource) AsInstanceObject() *states.ResourceInstanceObject { - return &states.ResourceInstanceObject{ - Status: states.ObjectReady, - Value: ir.State, - Private: ir.Private, - } -} - type ReadDataSourceRequest struct { // TypeName is the name of the data source type to Read. TypeName string diff --git a/internal/providers/schema_cache.go b/internal/providers/schema_cache.go index 670a0ecd213c..2b851335e3ae 100644 --- a/internal/providers/schema_cache.go +++ b/internal/providers/schema_cache.go @@ -12,13 +12,6 @@ import ( // SchemaCache is a global cache of Schemas. // This will be accessed by both core and the provider clients to ensure that // large schemas are stored in a single location. -// -// FIXME: A global cache is inappropriate when Terraform Core is being -// used in a non-Terraform-CLI mode where we shouldn't assume that all -// calls share the same provider implementations. This would be better -// as a per-terraform.Context cache instead, or to have callers preload -// the schemas for the providers they intend to use and pass them in -// to terraform.NewContext so we don't need to load them at runtime. var SchemaCache = &schemaCache{ m: make(map[addrs.Provider]ProviderSchema), } diff --git a/internal/providers/schemas.go b/internal/providers/schemas.go index 41b8bc6ee94b..083819df1e21 100644 --- a/internal/providers/schemas.go +++ b/internal/providers/schemas.go @@ -5,7 +5,6 @@ package providers import ( "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs/configschema" ) // ProviderSchema is an overall container for all of the schemas for all @@ -14,26 +13,25 @@ import ( type ProviderSchema = GetProviderSchemaResponse // SchemaForResourceType attempts to find a schema for the given mode and type. -// Returns nil if no such schema is available. -func (ss ProviderSchema) SchemaForResourceType(mode addrs.ResourceMode, typeName string) (schema *configschema.Block, version uint64) { +// Returns an empty schema if none is available. +func (ss ProviderSchema) SchemaForResourceType(mode addrs.ResourceMode, typeName string) (schema Schema) { switch mode { case addrs.ManagedResourceMode: - res := ss.ResourceTypes[typeName] - return res.Body, uint64(res.Version) + return ss.ResourceTypes[typeName] case addrs.DataResourceMode: - // Data resources don't have schema versions right now, since state is discarded for each refresh - return ss.DataSources[typeName].Body, 0 + return ss.DataSources[typeName] case addrs.EphemeralResourceMode: - // Ephemeral resources don't have schema versions because their objects never outlive a single phase - return ss.EphemeralResourceTypes[typeName].Body, 0 + return ss.EphemeralResourceTypes[typeName] default: // Shouldn't happen, because the above cases are comprehensive. - return nil, 0 + return Schema{} } } // SchemaForResourceAddr attempts to find a schema for the mode and type from -// the given resource address. Returns nil if no such schema is available. -func (ss ProviderSchema) SchemaForResourceAddr(addr addrs.Resource) (schema *configschema.Block, version uint64) { +// the given resource address. Returns an empty schema if none is available. +func (ss ProviderSchema) SchemaForResourceAddr(addr addrs.Resource) (schema Schema) { return ss.SchemaForResourceType(addr.Mode, addr.Type) } + +type ResourceIdentitySchemas = GetResourceIdentitySchemasResponse diff --git a/internal/providers/testing/provider_mock.go b/internal/providers/testing/provider_mock.go index c4f031ef1143..1e83280f9dd8 100644 --- a/internal/providers/testing/provider_mock.go +++ b/internal/providers/testing/provider_mock.go @@ -32,6 +32,9 @@ type MockProvider struct { GetProviderSchemaCalled bool GetProviderSchemaResponse *providers.GetProviderSchemaResponse + GetResourceIdentitySchemasCalled bool + GetResourceIdentitySchemasResponse *providers.GetResourceIdentitySchemasResponse + ValidateProviderConfigCalled bool ValidateProviderConfigResponse *providers.ValidateProviderConfigResponse ValidateProviderConfigRequest providers.ValidateProviderConfigRequest @@ -55,6 +58,12 @@ type MockProvider struct { UpgradeResourceStateRequest providers.UpgradeResourceStateRequest UpgradeResourceStateFn func(providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse + UpgradeResourceIdentityCalled bool + UpgradeResourceIdentityTypeName string + UpgradeResourceIdentityResponse *providers.UpgradeResourceIdentityResponse + UpgradeResourceIdentityRequest providers.UpgradeResourceIdentityRequest + UpgradeResourceIdentityFn func(providers.UpgradeResourceIdentityRequest) providers.UpgradeResourceIdentityResponse + ConfigureProviderCalled bool ConfigureProviderResponse *providers.ConfigureProviderResponse ConfigureProviderRequest providers.ConfigureProviderRequest @@ -142,6 +151,24 @@ func (p *MockProvider) getProviderSchema() providers.GetProviderSchemaResponse { } } +func (p *MockProvider) GetResourceIdentitySchemas() providers.GetResourceIdentitySchemasResponse { + p.Lock() + defer p.Unlock() + p.GetResourceIdentitySchemasCalled = true + + return p.getResourceIdentitySchemas() +} + +func (p *MockProvider) getResourceIdentitySchemas() providers.GetResourceIdentitySchemasResponse { + if p.GetResourceIdentitySchemasResponse != nil { + return *p.GetResourceIdentitySchemasResponse + } + + return providers.GetResourceIdentitySchemasResponse{ + IdentityTypes: map[string]providers.IdentitySchema{}, + } +} + func (p *MockProvider) ValidateProviderConfig(r providers.ValidateProviderConfigRequest) (resp providers.ValidateProviderConfigResponse) { p.Lock() defer p.Unlock() @@ -306,6 +333,44 @@ func (p *MockProvider) UpgradeResourceState(r providers.UpgradeResourceStateRequ return resp } +func (p *MockProvider) UpgradeResourceIdentity(r providers.UpgradeResourceIdentityRequest) (resp providers.UpgradeResourceIdentityResponse) { + p.Lock() + defer p.Unlock() + + if !p.ConfigureProviderCalled { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("Configure not called before UpgradeResourceIdentity %q", r.TypeName)) + return resp + } + p.UpgradeResourceIdentityCalled = true + p.UpgradeResourceIdentityRequest = r + + if p.UpgradeResourceIdentityFn != nil { + return p.UpgradeResourceIdentityFn(r) + } + + if p.UpgradeResourceIdentityResponse != nil { + return *p.UpgradeResourceIdentityResponse + } + + identitySchema, ok := p.getResourceIdentitySchemas().IdentityTypes[r.TypeName] + if !ok { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("no schema found for %q", r.TypeName)) + return resp + } + + identityType := identitySchema.Body.ImpliedType() + + v, err := ctyjson.Unmarshal(r.RawIdentityJSON, identityType) + + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + resp.UpgradedIdentity = v + + return resp +} + func (p *MockProvider) ConfigureProvider(r providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { p.Lock() defer p.Unlock() diff --git a/internal/refactoring/cross_provider_move.go b/internal/refactoring/cross_provider_move.go index d0bc6c2af2ea..1d60aa9c1a4b 100644 --- a/internal/refactoring/cross_provider_move.go +++ b/internal/refactoring/cross_provider_move.go @@ -10,7 +10,6 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/lang/ephemeral" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" @@ -100,21 +99,19 @@ func (m *crossTypeMover) prepareCrossTypeMove(stmt *MoveStatement, source, targe }) return nil, diags } - targetResourceSchema, targetResourceSchemaVersion := targetSchema.SchemaForResourceAddr(target.Resource) + schema := targetSchema.SchemaForResourceAddr(target.Resource) return &crossTypeMove{ - targetProvider: targetProvider, - targetProviderAddr: *targetProviderAddr, - targetResourceSchema: targetResourceSchema, - targetResourceSchemaVersion: targetResourceSchemaVersion, - sourceProviderAddr: sourceProviderAddr, + targetProvider: targetProvider, + targetProviderAddr: *targetProviderAddr, + targetResourceSchema: schema, + sourceProviderAddr: sourceProviderAddr, }, diags } type crossTypeMove struct { - targetProvider providers.Interface - targetProviderAddr addrs.AbsProviderConfig - targetResourceSchema *configschema.Block - targetResourceSchemaVersion uint64 + targetProvider providers.Interface + targetProviderAddr addrs.AbsProviderConfig + targetResourceSchema providers.Schema sourceProviderAddr addrs.AbsProviderConfig } @@ -162,7 +159,7 @@ func (move *crossTypeMove) applyCrossTypeMove(stmt *MoveStatement, source, targe ) }, resp.TargetState, - move.targetResourceSchema, + move.targetResourceSchema.Body, ) diags = diags.Append(writeOnlyDiags) @@ -199,7 +196,8 @@ func (move *crossTypeMove) applyCrossTypeMove(stmt *MoveStatement, source, targe CreateBeforeDestroy: src.CreateBeforeDestroy, } - data, err := newValue.Encode(move.targetResourceSchema.ImpliedType(), move.targetResourceSchemaVersion) + // TODO: We need to handle identity data in move scenarios. + data, err := newValue.Encode(move.targetResourceSchema) if err != nil { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, diff --git a/internal/refactoring/mock_provider.go b/internal/refactoring/mock_provider.go index 45648ffc4a11..7304c0ba7ede 100644 --- a/internal/refactoring/mock_provider.go +++ b/internal/refactoring/mock_provider.go @@ -29,6 +29,10 @@ func (provider *mockProvider) GetProviderSchema() providers.GetProviderSchemaRes } } +func (provider *mockProvider) GetResourceIdentitySchemas() providers.GetResourceIdentitySchemasResponse { + panic("not implemented in mock") +} + func (provider *mockProvider) ValidateProviderConfig(providers.ValidateProviderConfigRequest) providers.ValidateProviderConfigResponse { panic("not implemented in mock") } @@ -45,6 +49,10 @@ func (provider *mockProvider) UpgradeResourceState(providers.UpgradeResourceStat panic("not implemented in mock") } +func (provider *mockProvider) UpgradeResourceIdentity(providers.UpgradeResourceIdentityRequest) providers.UpgradeResourceIdentityResponse { + panic("not implemented in mock") +} + func (provider *mockProvider) ConfigureProvider(providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { panic("not implemented in mock") } diff --git a/internal/schemarepo/loadschemas/plugins.go b/internal/schemarepo/loadschemas/plugins.go index c383655ac231..5408adece0e7 100644 --- a/internal/schemarepo/loadschemas/plugins.go +++ b/internal/schemarepo/loadschemas/plugins.go @@ -100,10 +100,6 @@ func (cp *Plugins) ProviderSchema(addr addrs.Provider) (providers.ProviderSchema // We skip this if we have preloaded schemas because that suggests that // our caller is not Terraform CLI and therefore it's probably inappropriate // to assume that provider schemas are unique process-wide. - // - // FIXME: A global cache is inappropriate when Terraform Core is being - // used in a non-Terraform-CLI mode where we shouldn't assume that all - // calls share the same provider implementations. schemas, ok := providers.SchemaCache.Get(addr) if ok { log.Printf("[TRACE] terraform.contextPlugins: Schema for provider %q is in the global cache", addr) @@ -141,6 +137,30 @@ func (cp *Plugins) ProviderSchema(addr addrs.Provider) (providers.ProviderSchema if r.Version < 0 { return resp, fmt.Errorf("provider %s has invalid negative schema version for managed resource type %q, which is a bug in the provider", addr, t) } + + // Validate resource identity schema if the resource has one + if r.Identity != nil { + if err := r.Identity.InternalValidate(); err != nil { + return resp, fmt.Errorf("provider %s has invalid identity schema for managed resource type %q, which is a bug in the provider: %q", addr, t, err) + } + if r.IdentityVersion < 0 { + return resp, fmt.Errorf("provider %s has invalid negative identity schema version for managed resource type %q, which is a bug in the provider", addr, t) + } + + for attrName, attrTy := range r.Identity.ImpliedType().AttributeTypes() { + if attrTy.MapElementType() != nil { + return resp, fmt.Errorf("provider %s has invalid schema for managed resource type %q, attribute %q is a map, which is not allowed in identity schemas", addr, t, attrName) + } + + if attrTy.SetElementType() != nil { + return resp, fmt.Errorf("provider %s has invalid schema for managed resource type %q, attribute %q is a set, which is not allowed in identity schemas", addr, t, attrName) + } + + if attrTy.IsObjectType() { + return resp, fmt.Errorf("provider %s has invalid schema for managed resource type %q, attribute %q is an object, which is not allowed in identity schemas", addr, t, attrName) + } + } + } } for t, d := range resp.DataSources { @@ -208,20 +228,15 @@ func (cp *Plugins) ProviderConfigSchema(providerAddr addrs.Provider) (*configsch // for the resource type of the given resource mode in that provider. // // ResourceTypeSchema will return an error if the provider schema lookup -// fails, but will return nil if the provider schema lookup succeeds but then -// the provider doesn't have a resource of the requested type. -// -// Managed resource types have versioned schemas, so the second return value -// is the current schema version number for the requested resource. The version -// is irrelevant for other resource modes. -func (cp *Plugins) ResourceTypeSchema(providerAddr addrs.Provider, resourceMode addrs.ResourceMode, resourceType string) (*configschema.Block, uint64, error) { +// fails, but will return an empty schema if the provider schema lookup +// succeeds but then the provider doesn't have a resource of the requested type. +func (cp *Plugins) ResourceTypeSchema(providerAddr addrs.Provider, resourceMode addrs.ResourceMode, resourceType string) (providers.Schema, error) { providerSchema, err := cp.ProviderSchema(providerAddr) if err != nil { - return nil, 0, err + return providers.Schema{}, err } - schema, version := providerSchema.SchemaForResourceType(resourceMode, resourceType) - return schema, version, nil + return providerSchema.SchemaForResourceType(resourceMode, resourceType), nil } // ProvisionerSchema uses a temporary instance of the provisioner with the diff --git a/internal/schemarepo/schemas.go b/internal/schemarepo/schemas.go index 81616c2c1cb1..b076d80c02d7 100644 --- a/internal/schemarepo/schemas.go +++ b/internal/schemarepo/schemas.go @@ -32,18 +32,18 @@ func (ss *Schemas) ProviderConfig(provider addrs.Provider) *configschema.Block { } // ResourceTypeConfig returns the schema for the configuration of a given -// resource type belonging to a given provider type, or nil of no such -// schema is available. +// resource type belonging to a given provider type, or an empty schema +// if no such schema is available. // // In many cases the provider type is inferrable from the resource type name, // but this is not always true because users can override the provider for // a resource using the "provider" meta-argument. Therefore it's important to // always pass the correct provider name, even though it many cases it feels // redundant. -func (ss *Schemas) ResourceTypeConfig(provider addrs.Provider, resourceMode addrs.ResourceMode, resourceType string) (block *configschema.Block, schemaVersion uint64) { +func (ss *Schemas) ResourceTypeConfig(provider addrs.Provider, resourceMode addrs.ResourceMode, resourceType string) providers.Schema { ps := ss.ProviderSchema(provider) if ps.ResourceTypes == nil { - return nil, 0 + return providers.Schema{} } return ps.SchemaForResourceType(resourceMode, resourceType) } diff --git a/internal/stacks/stackplan/from_plan.go b/internal/stacks/stackplan/from_plan.go index 31902eddefc4..43d58c25ae4c 100644 --- a/internal/stacks/stackplan/from_plan.go +++ b/internal/stacks/stackplan/from_plan.go @@ -12,9 +12,9 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/collections" "github.com/hashicorp/terraform/internal/configs" - "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" @@ -36,7 +36,7 @@ type PlanProducer interface { RequiredComponents(ctx context.Context) collections.Set[stackaddrs.AbsComponent] // ResourceSchema returns the schema for a resource type from a provider. - ResourceSchema(ctx context.Context, providerTypeAddr addrs.Provider, mode addrs.ResourceMode, resourceType string) (*configschema.Block, error) + ResourceSchema(ctx context.Context, providerTypeAddr addrs.Provider, mode addrs.ResourceMode, resourceType string) (providers.Schema, error) } func FromPlan(ctx context.Context, config *configs.Config, plan *plans.Plan, refreshPlan *plans.Plan, action plans.Action, producer PlanProducer) ([]PlannedChange, tfdiags.Diagnostics) { diff --git a/internal/stacks/stackplan/planned_change.go b/internal/stacks/stackplan/planned_change.go index ca60f82c5856..0575142b1084 100644 --- a/internal/stacks/stackplan/planned_change.go +++ b/internal/stacks/stackplan/planned_change.go @@ -15,7 +15,6 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/collections" - "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/plans/planfile" @@ -364,9 +363,9 @@ type PlannedChangeResourceInstancePlanned struct { // Schema MUST be the same schema that was used to encode the dynamic // values inside ChangeSrc and PriorStateSrc. // - // Can be nil if and only if ChangeSrc and PriorStateSrc are both nil + // Can be empty if and only if ChangeSrc and PriorStateSrc are both nil // themselves. - Schema *configschema.Block + Schema providers.Schema } var _ PlannedChange = (*PlannedChangeResourceInstancePlanned)(nil) diff --git a/internal/stacks/stackruntime/apply_destroy_test.go b/internal/stacks/stackruntime/apply_destroy_test.go index ef3c7d411005..3555c665df21 100644 --- a/internal/stacks/stackruntime/apply_destroy_test.go +++ b/internal/stacks/stackruntime/apply_destroy_test.go @@ -128,7 +128,7 @@ func TestApplyDestroy(t *testing.T) { ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), ProviderConfigAddr: mustDefaultRootProvider("testing"), NewStateSrc: nil, // We should be removing this from the state file. - Schema: nil, + Schema: providers.Schema{}, }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("id"), @@ -185,7 +185,7 @@ func TestApplyDestroy(t *testing.T) { &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.data.testing_data_source.data"), ProviderConfigAddr: mustDefaultRootProvider("testing"), - Schema: nil, + Schema: providers.Schema{}, NewStateSrc: nil, // deleted }, @@ -193,7 +193,7 @@ func TestApplyDestroy(t *testing.T) { &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.data.testing_data_source.missing"), ProviderConfigAddr: mustDefaultRootProvider("testing"), - Schema: nil, + Schema: providers.Schema{}, NewStateSrc: nil, }, &stackstate.AppliedChangeInputVariable{ @@ -264,7 +264,7 @@ func TestApplyDestroy(t *testing.T) { &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.data.testing_data_source.missing"), ProviderConfigAddr: mustDefaultRootProvider("testing"), - Schema: nil, + Schema: providers.Schema{}, NewStateSrc: nil, // deleted }, &stackstate.AppliedChangeResourceInstanceObject{ @@ -306,13 +306,13 @@ func TestApplyDestroy(t *testing.T) { &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.data.testing_data_source.data"), ProviderConfigAddr: mustDefaultRootProvider("testing"), - Schema: nil, + Schema: providers.Schema{}, NewStateSrc: nil, // deleted }, &stackstate.AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), ProviderConfigAddr: mustDefaultRootProvider("testing"), - Schema: nil, + Schema: providers.Schema{}, NewStateSrc: nil, // deleted }, &stackstate.AppliedChangeInputVariable{ diff --git a/internal/stacks/stackruntime/apply_test.go b/internal/stacks/stackruntime/apply_test.go index ca6f44e1baed..a7e05bce675e 100644 --- a/internal/stacks/stackruntime/apply_test.go +++ b/internal/stacks/stackruntime/apply_test.go @@ -442,7 +442,7 @@ func TestApply(t *testing.T) { ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), ProviderConfigAddr: mustDefaultRootProvider("testing"), NewStateSrc: nil, - Schema: nil, + Schema: providers.Schema{}, }, }, }, @@ -625,7 +625,7 @@ func TestApply(t *testing.T) { ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"removed\"].testing_resource.data"), ProviderConfigAddr: mustDefaultRootProvider("testing"), NewStateSrc: nil, - Schema: nil, + Schema: providers.Schema{}, }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("input"), @@ -735,7 +735,7 @@ func TestApply(t *testing.T) { ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.a.component.self.testing_resource.data"), ProviderConfigAddr: mustDefaultRootProvider("testing"), NewStateSrc: nil, - Schema: nil, + Schema: providers.Schema{}, }, }, }, @@ -846,7 +846,7 @@ After applying this plan, Terraform will no longer manage these objects. You wil ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), ProviderConfigAddr: mustDefaultRootProvider("testing"), NewStateSrc: nil, - Schema: nil, + Schema: providers.Schema{}, }, }, }, @@ -3969,7 +3969,7 @@ func TestApplyManuallyRemovedResource(t *testing.T) { ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.missing"), ProviderConfigAddr: mustDefaultRootProvider("testing"), NewStateSrc: nil, // We should be removing this from the state file. - Schema: nil, + Schema: providers.Schema{}, }, &stackstate.AppliedChangeInputVariable{ Addr: mustStackInputVariable("id"), diff --git a/internal/stacks/stackruntime/internal/stackeval/component_instance.go b/internal/stacks/stackruntime/internal/stackeval/component_instance.go index e84c04b40835..c2c8034aaa0f 100644 --- a/internal/stacks/stackruntime/internal/stackeval/component_instance.go +++ b/internal/stacks/stackruntime/internal/stackeval/component_instance.go @@ -14,12 +14,12 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/collections" "github.com/hashicorp/terraform/internal/configs" - "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" "github.com/hashicorp/terraform/internal/stacks/stackplan" "github.com/hashicorp/terraform/internal/stacks/stackstate" @@ -734,7 +734,7 @@ func (c *ComponentInstance) CheckApply(ctx context.Context) ([]stackstate.Applie } // ResourceSchema implements stackplan.PlanProducer. -func (c *ComponentInstance) ResourceSchema(ctx context.Context, providerTypeAddr addrs.Provider, mode addrs.ResourceMode, typ string) (*configschema.Block, error) { +func (c *ComponentInstance) ResourceSchema(ctx context.Context, providerTypeAddr addrs.Provider, mode addrs.ResourceMode, typ string) (providers.Schema, error) { // This should not be able to fail with an error because we should // be retrieving the same schema that was already used to encode // the object we're working with. The error handling here is for @@ -743,11 +743,11 @@ func (c *ComponentInstance) ResourceSchema(ctx context.Context, providerTypeAddr providerType := c.main.ProviderType(ctx, providerTypeAddr) providerSchema, err := providerType.Schema(ctx) if err != nil { - return nil, err + return providers.Schema{}, err } - ret, _ := providerSchema.SchemaForResourceType(mode, typ) - if ret == nil { - return nil, fmt.Errorf("schema does not include %v %q", mode, typ) + ret := providerSchema.SchemaForResourceType(mode, typ) + if ret.Body == nil { + return providers.Schema{}, fmt.Errorf("schema does not include %v %q", mode, typ) } return ret, nil } diff --git a/internal/stacks/stackruntime/internal/stackeval/removed_instance.go b/internal/stacks/stackruntime/internal/stackeval/removed_instance.go index 9c860979fa54..c1915b6d9a95 100644 --- a/internal/stacks/stackruntime/internal/stackeval/removed_instance.go +++ b/internal/stacks/stackruntime/internal/stackeval/removed_instance.go @@ -14,11 +14,11 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/collections" "github.com/hashicorp/terraform/internal/configs" - "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" "github.com/hashicorp/terraform/internal/stacks/stackplan" "github.com/hashicorp/terraform/internal/stacks/stackstate" @@ -323,7 +323,7 @@ func (r *RemovedInstance) RequiredComponents(ctx context.Context) collections.Se } // ResourceSchema implements stackplan.PlanProducer. -func (r *RemovedInstance) ResourceSchema(ctx context.Context, providerTypeAddr addrs.Provider, mode addrs.ResourceMode, typ string) (*configschema.Block, error) { +func (r *RemovedInstance) ResourceSchema(ctx context.Context, providerTypeAddr addrs.Provider, mode addrs.ResourceMode, typ string) (providers.Schema, error) { // This should not be able to fail with an error because we should // be retrieving the same schema that was already used to encode // the object we're working with. The error handling here is for @@ -332,11 +332,11 @@ func (r *RemovedInstance) ResourceSchema(ctx context.Context, providerTypeAddr a providerType := r.main.ProviderType(ctx, providerTypeAddr) providerSchema, err := providerType.Schema(ctx) if err != nil { - return nil, err + return providers.Schema{}, err } - ret, _ := providerSchema.SchemaForResourceType(mode, typ) - if ret == nil { - return nil, fmt.Errorf("schema does not include %v %q", mode, typ) + ret := providerSchema.SchemaForResourceType(mode, typ) + if ret.Body == nil { + return providers.Schema{}, fmt.Errorf("schema does not include %v %q", mode, typ) } return ret, nil } diff --git a/internal/stacks/stackruntime/internal/stackeval/stubs/errored.go b/internal/stacks/stackruntime/internal/stackeval/stubs/errored.go index a1b254dd08d5..b03addf24328 100644 --- a/internal/stacks/stackruntime/internal/stackeval/stubs/errored.go +++ b/internal/stacks/stackruntime/internal/stackeval/stubs/errored.go @@ -60,6 +60,10 @@ func (p *erroredProvider) GetProviderSchema() providers.GetProviderSchemaRespons return providers.GetProviderSchemaResponse{} } +func (p *erroredProvider) GetResourceIdentitySchemas() providers.GetResourceIdentitySchemasResponse { + return providers.GetResourceIdentitySchemasResponse{} +} + // ImportResourceState implements providers.Interface. func (p *erroredProvider) ImportResourceState(req providers.ImportResourceStateRequest) providers.ImportResourceStateResponse { var diags tfdiags.Diagnostics @@ -180,6 +184,22 @@ func (p *erroredProvider) UpgradeResourceState(req providers.UpgradeResourceStat } } +func (p *erroredProvider) UpgradeResourceIdentity(req providers.UpgradeResourceIdentityRequest) providers.UpgradeResourceIdentityResponse { + // Ideally we'd just skip this altogether and echo back what the caller + // provided, but the request is in a different serialization format than + // the response and so only the real provider can deal with this one. + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Provider configuration is invalid", + "Cannot decode the prior state for this resource instance because its provider configuration is invalid.", + nil, // nil attribute path means the overall configuration block + )) + return providers.UpgradeResourceIdentityResponse{ + Diagnostics: diags, + } +} + // ValidateDataResourceConfig implements providers.Interface. func (p *erroredProvider) ValidateDataResourceConfig(req providers.ValidateDataResourceConfigRequest) providers.ValidateDataResourceConfigResponse { // We'll just optimistically assume the configuration is valid, so that diff --git a/internal/stacks/stackruntime/internal/stackeval/stubs/offline.go b/internal/stacks/stackruntime/internal/stackeval/stubs/offline.go index e2b06843e746..e594586398d8 100644 --- a/internal/stacks/stackruntime/internal/stackeval/stubs/offline.go +++ b/internal/stacks/stackruntime/internal/stackeval/stubs/offline.go @@ -33,6 +33,10 @@ func (o *offlineProvider) GetProviderSchema() providers.GetProviderSchemaRespons return o.unconfiguredClient.GetProviderSchema() } +func (o *offlineProvider) GetResourceIdentitySchemas() providers.GetResourceIdentitySchemasResponse { + return o.unconfiguredClient.GetResourceIdentitySchemas() +} + func (o *offlineProvider) ValidateProviderConfig(request providers.ValidateProviderConfigRequest) providers.ValidateProviderConfigResponse { var diags tfdiags.Diagnostics diags = diags.Append(tfdiags.AttributeValue( @@ -99,6 +103,19 @@ func (o *offlineProvider) UpgradeResourceState(request providers.UpgradeResource } } +func (o *offlineProvider) UpgradeResourceIdentity(request providers.UpgradeResourceIdentityRequest) providers.UpgradeResourceIdentityResponse { + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Called UpgradeResourceIdentity on an unconfigured provider", + "Cannot upgrade the state of this resource because this provider is not configured. This is a bug in Terraform - please report it.", + nil, // nil attribute path means the overall configuration block + )) + return providers.UpgradeResourceIdentityResponse{ + Diagnostics: diags, + } +} + func (o *offlineProvider) ConfigureProvider(request providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { var diags tfdiags.Diagnostics diags = diags.Append(tfdiags.AttributeValue( diff --git a/internal/stacks/stackruntime/internal/stackeval/stubs/unknown.go b/internal/stacks/stackruntime/internal/stackeval/stubs/unknown.go index 585da11b8a98..2b681755cb7a 100644 --- a/internal/stacks/stackruntime/internal/stackeval/stubs/unknown.go +++ b/internal/stacks/stackruntime/internal/stackeval/stubs/unknown.go @@ -39,6 +39,10 @@ func (u *unknownProvider) GetProviderSchema() providers.GetProviderSchemaRespons return u.unconfiguredClient.GetProviderSchema() } +func (u *unknownProvider) GetResourceIdentitySchemas() providers.GetResourceIdentitySchemasResponse { + return u.unconfiguredClient.GetResourceIdentitySchemas() +} + func (u *unknownProvider) ValidateProviderConfig(request providers.ValidateProviderConfigRequest) providers.ValidateProviderConfigResponse { // This is offline functionality, so we can hand it off to the unconfigured // client. @@ -70,6 +74,12 @@ func (u *unknownProvider) UpgradeResourceState(request providers.UpgradeResource return u.unconfiguredClient.UpgradeResourceState(request) } +func (u *unknownProvider) UpgradeResourceIdentity(request providers.UpgradeResourceIdentityRequest) providers.UpgradeResourceIdentityResponse { + // This is offline functionality, so we can hand it off to the unconfigured + // client. + return u.unconfiguredClient.UpgradeResourceIdentity(request) +} + func (u *unknownProvider) ConfigureProvider(request providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { // This shouldn't be called, we don't configure an unknown provider within // stacks and Terraform Core shouldn't call this method. diff --git a/internal/stacks/stackruntime/plan_test.go b/internal/stacks/stackruntime/plan_test.go index e708579cc7dd..01e5a35adf9a 100644 --- a/internal/stacks/stackruntime/plan_test.go +++ b/internal/stacks/stackruntime/plan_test.go @@ -1081,12 +1081,24 @@ func TestPlanWithSingleResource(t *testing.T) { // type from the real terraform.io/builtin/terraform provider // maintained elsewhere in this codebase. If that schema changes // in future then this should change to match it. - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "input": {Type: cty.DynamicPseudoType, Optional: true}, - "output": {Type: cty.DynamicPseudoType, Computed: true}, - "triggers_replace": {Type: cty.DynamicPseudoType, Optional: true}, - "id": {Type: cty.String, Computed: true}, + Schema: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "input": {Type: cty.DynamicPseudoType, Optional: true}, + "output": {Type: cty.DynamicPseudoType, Computed: true}, + "triggers_replace": {Type: cty.DynamicPseudoType, Optional: true}, + "id": {Type: cty.String, Computed: true}, + }, + }, + Identity: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Description: "The unique identifier for the data store.", + Required: true, + }, + }, + Nesting: configschema.NestingSingle, }, }, }, @@ -1843,7 +1855,7 @@ func TestPlanWithSensitivePropagation(t *testing.T) { After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.StringVal("secret"), - }), stacks_testing_provider.TestingResourceSchema), + }), stacks_testing_provider.TestingResourceSchema.Body), AfterSensitivePaths: []cty.Path{ cty.GetAttrPath("value"), }, @@ -2006,7 +2018,7 @@ func TestPlanWithSensitivePropagationNested(t *testing.T) { After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.StringVal("secret"), - }), stacks_testing_provider.TestingResourceSchema), + }), stacks_testing_provider.TestingResourceSchema.Body), AfterSensitivePaths: []cty.Path{ cty.GetAttrPath("value"), }, @@ -2324,7 +2336,7 @@ func TestPlanWithCheckableObjects(t *testing.T) { After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("test"), "value": cty.StringVal("bar"), - }), stacks_testing_provider.TestingResourceSchema), + }), stacks_testing_provider.TestingResourceSchema.Body), }, }, @@ -2458,7 +2470,7 @@ func TestPlanWithDeferredResource(t *testing.T) { "id": cty.StringVal("62594ae3"), "value": cty.NullVal(cty.String), "deferred": cty.BoolVal(true), - }), stacks_testing_provider.DeferredResourceSchema), + }), stacks_testing_provider.DeferredResourceSchema.Body), AfterSensitivePaths: nil, }, }, @@ -2623,7 +2635,7 @@ func TestPlanWithDeferredComponentForEach(t *testing.T) { After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.UnknownVal(cty.String), - }), stacks_testing_provider.TestingResourceSchema), + }), stacks_testing_provider.TestingResourceSchema.Body), AfterSensitivePaths: nil, }, }, @@ -2704,7 +2716,7 @@ func TestPlanWithDeferredComponentForEach(t *testing.T) { After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.UnknownVal(cty.String), - }), stacks_testing_provider.TestingResourceSchema), + }), stacks_testing_provider.TestingResourceSchema.Body), AfterSensitivePaths: nil, }, }, @@ -2865,7 +2877,7 @@ func TestPlanWithDeferredComponentReferences(t *testing.T) { After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.UnknownVal(cty.String), - }), stacks_testing_provider.TestingResourceSchema), + }), stacks_testing_provider.TestingResourceSchema.Body), AfterSensitivePaths: nil, }, }, @@ -2950,7 +2962,7 @@ func TestPlanWithDeferredComponentReferences(t *testing.T) { After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.StringVal("known"), - }), stacks_testing_provider.TestingResourceSchema), + }), stacks_testing_provider.TestingResourceSchema.Body), }, ProviderAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, @@ -3116,7 +3128,7 @@ func TestPlanWithDeferredEmbeddedStackForEach(t *testing.T) { After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.UnknownVal(cty.String), - }), stacks_testing_provider.TestingResourceSchema), + }), stacks_testing_provider.TestingResourceSchema.Body), AfterSensitivePaths: nil, }, }, @@ -3266,7 +3278,7 @@ func TestPlanWithDeferredEmbeddedStackAndComponentForEach(t *testing.T) { After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.UnknownVal(cty.String), - }), stacks_testing_provider.TestingResourceSchema), + }), stacks_testing_provider.TestingResourceSchema.Body), AfterSensitivePaths: nil, }, }, @@ -3465,7 +3477,7 @@ func TestPlanWithDeferredProviderForEach(t *testing.T) { After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.StringVal("primary"), - }), stacks_testing_provider.TestingResourceSchema), + }), stacks_testing_provider.TestingResourceSchema.Body), }, }, Schema: stacks_testing_provider.TestingResourceSchema, @@ -3537,7 +3549,7 @@ func TestPlanWithDeferredProviderForEach(t *testing.T) { After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.StringVal("secondary"), - }), stacks_testing_provider.TestingResourceSchema), + }), stacks_testing_provider.TestingResourceSchema.Body), }, }, Schema: stacks_testing_provider.TestingResourceSchema, diff --git a/internal/stacks/stackruntime/testing/provider.go b/internal/stacks/stackruntime/testing/provider.go index 037d13741211..fc008bbc3600 100644 --- a/internal/stacks/stackruntime/testing/provider.go +++ b/internal/stacks/stackruntime/testing/provider.go @@ -19,42 +19,52 @@ import ( ) var ( - TestingResourceSchema = &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "value": {Type: cty.String, Optional: true}, + TestingResourceSchema = providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "value": {Type: cty.String, Optional: true}, + }, }, } - DeferredResourceSchema = &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "value": {Type: cty.String, Optional: true}, - "deferred": {Type: cty.Bool, Required: true}, + DeferredResourceSchema = providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "value": {Type: cty.String, Optional: true}, + "deferred": {Type: cty.Bool, Required: true}, + }, }, } - FailedResourceSchema = &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "value": {Type: cty.String, Optional: true}, - "fail_plan": {Type: cty.Bool, Optional: true, Computed: true}, - "fail_apply": {Type: cty.Bool, Optional: true, Computed: true}, + FailedResourceSchema = providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "value": {Type: cty.String, Optional: true}, + "fail_plan": {Type: cty.Bool, Optional: true, Computed: true}, + "fail_apply": {Type: cty.Bool, Optional: true, Computed: true}, + }, }, } - BlockedResourceSchema = &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "value": {Type: cty.String, Optional: true}, - "required_resources": {Type: cty.Set(cty.String), Optional: true}, + BlockedResourceSchema = providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "value": {Type: cty.String, Optional: true}, + "required_resources": {Type: cty.Set(cty.String), Optional: true}, + }, }, } - TestingDataSourceSchema = &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Required: true}, - "value": {Type: cty.String, Computed: true}, + TestingDataSourceSchema = providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Required: true}, + "value": {Type: cty.String, Computed: true}, + }, }, } ) @@ -127,21 +137,21 @@ func NewProviderWithData(t *testing.T, store *ResourceStore) *MockProvider { }, ResourceTypes: map[string]providers.Schema{ "testing_resource": { - Body: TestingResourceSchema, + Body: TestingResourceSchema.Body, }, "testing_deferred_resource": { - Body: DeferredResourceSchema, + Body: DeferredResourceSchema.Body, }, "testing_failed_resource": { - Body: FailedResourceSchema, + Body: FailedResourceSchema.Body, }, "testing_blocked_resource": { - Body: BlockedResourceSchema, + Body: BlockedResourceSchema.Body, }, }, DataSources: map[string]providers.Schema{ "testing_data_source": { - Body: TestingDataSourceSchema, + Body: TestingDataSourceSchema.Body, }, }, Functions: map[string]providers.FunctionDecl{ diff --git a/internal/stacks/stackruntime/testing/resource.go b/internal/stacks/stackruntime/testing/resource.go index ebb861a8d929..617d88a4200b 100644 --- a/internal/stacks/stackruntime/testing/resource.go +++ b/internal/stacks/stackruntime/testing/resource.go @@ -56,7 +56,7 @@ func (t *testingResource) Read(request providers.ReadResourceRequest, store *Res var exists bool response.NewState, exists = store.Get(id) if !exists { - response.NewState = cty.NullVal(TestingResourceSchema.ImpliedType()) + response.NewState = cty.NullVal(TestingResourceSchema.Body.ImpliedType()) } return } @@ -110,7 +110,7 @@ func (d *deferredResource) Read(request providers.ReadResourceRequest, store *Re var exists bool response.NewState, exists = store.Get(id) if !exists { - response.NewState = cty.NullVal(DeferredResourceSchema.ImpliedType()) + response.NewState = cty.NullVal(DeferredResourceSchema.Body.ImpliedType()) } return } @@ -173,7 +173,7 @@ func (f *failedResource) Read(request providers.ReadResourceRequest, store *Reso var exists bool response.NewState, exists = store.Get(id) if !exists { - response.NewState = cty.NullVal(FailedResourceSchema.ImpliedType()) + response.NewState = cty.NullVal(FailedResourceSchema.Body.ImpliedType()) } return } @@ -253,7 +253,7 @@ func (b *blockedResource) Read(request providers.ReadResourceRequest, store *Res var exists bool response.NewState, exists = store.Get(id) if !exists { - response.NewState = cty.NullVal(DeferredResourceSchema.ImpliedType()) + response.NewState = cty.NullVal(DeferredResourceSchema.Body.ImpliedType()) } return } diff --git a/internal/stacks/stackstate/applied_change.go b/internal/stacks/stackstate/applied_change.go index 4dc70d67fd33..5ada555be1f9 100644 --- a/internal/stacks/stackstate/applied_change.go +++ b/internal/stacks/stackstate/applied_change.go @@ -12,7 +12,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/collections" - "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" "github.com/hashicorp/terraform/internal/stacks/stackstate/statekeys" @@ -59,9 +59,9 @@ type AppliedChangeResourceInstanceObject struct { PreviousResourceInstanceObjectAddr *stackaddrs.AbsResourceInstanceObject // Schema MUST be the same schema that was used to encode the dynamic - // values inside NewStateSrc. This can be left as nil if NewStateSrc + // values inside NewStateSrc. This can be left as empty if NewStateSrc // is nil, which represents that the object has been deleted. - Schema *configschema.Block + Schema providers.Schema } var _ AppliedChange = (*AppliedChangeResourceInstanceObject)(nil) @@ -147,8 +147,7 @@ func (ac *AppliedChangeResourceInstanceObject) protosForObject() ([]*stacks.Appl // exclusively uses MessagePack encoding for dynamic // values, and so we will need to use the ac.Schema to // transcode the data. - ty := ac.Schema.ImpliedType() - obj, err := objSrc.Decode(ty) + obj, err := objSrc.Decode(ac.Schema) if err != nil { // It would be _very_ strange to get here because we should just // be reversing the same encoding operation done earlier to @@ -156,7 +155,7 @@ func (ac *AppliedChangeResourceInstanceObject) protosForObject() ([]*stacks.Appl return nil, nil, fmt.Errorf("cannot decode new state for %s in preparation for saving it: %w", addr, err) } - protoValue, err := stacks.ToDynamicValue(obj.Value, ty) + protoValue, err := stacks.ToDynamicValue(obj.Value, ac.Schema.Body.ImpliedType()) if err != nil { return nil, nil, fmt.Errorf("cannot encode new state for %s in preparation for saving it: %w", addr, err) } diff --git a/internal/stacks/stackstate/applied_change_test.go b/internal/stacks/stackstate/applied_change_test.go index 14d576f2c576..780599e1513e 100644 --- a/internal/stacks/stackstate/applied_change_test.go +++ b/internal/stacks/stackstate/applied_change_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/plans/planproto" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" "github.com/hashicorp/terraform/internal/stacks/tfstackdata1" @@ -52,15 +53,17 @@ func TestAppliedChangeAsProto(t *testing.T) { Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("example.com/thingers/thingy"), }, - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": { - Type: cty.String, - Required: true, - }, - "secret": { - Type: cty.String, - Sensitive: true, + Schema: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + "secret": { + Type: cty.String, + Sensitive: true, + }, }, }, }, @@ -160,15 +163,17 @@ func TestAppliedChangeAsProto(t *testing.T) { Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("example.com/thingers/thingy"), }, - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": { - Type: cty.String, - Required: true, - }, - "secret": { - Type: cty.String, - Sensitive: true, + Schema: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + "secret": { + Type: cty.String, + Sensitive: true, + }, }, }, }, diff --git a/internal/stacks/stackstate/from_state.go b/internal/stacks/stackstate/from_state.go index 2680d4dd37f3..c8030f107685 100644 --- a/internal/stacks/stackstate/from_state.go +++ b/internal/stacks/stackstate/from_state.go @@ -10,9 +10,9 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" "github.com/hashicorp/terraform/internal/stacks/stackplan" "github.com/hashicorp/terraform/internal/states" @@ -25,7 +25,7 @@ type StateProducer interface { Addr() stackaddrs.AbsComponentInstance // ResourceSchema returns the schema for a resource type from a provider. - ResourceSchema(ctx context.Context, providerTypeAddr addrs.Provider, mode addrs.ResourceMode, resourceType string) (*configschema.Block, error) + ResourceSchema(ctx context.Context, providerTypeAddr addrs.Provider, mode addrs.ResourceMode, resourceType string) (providers.Schema, error) } func FromState(ctx context.Context, state *states.State, plan *stackplan.Component, applyTimeInputs cty.Value, affectedResources addrs.Set[addrs.AbsResourceInstanceObject], producer StateProducer) ([]AppliedChange, tfdiags.Diagnostics) { @@ -37,7 +37,7 @@ func FromState(ctx context.Context, state *states.State, plan *stackplan.Compone for _, rioAddr := range affectedResources { os := state.ResourceInstanceObjectSrc(rioAddr) var providerConfigAddr addrs.AbsProviderConfig - var schema *configschema.Block + var schema providers.Schema if os != nil { rAddr := rioAddr.ResourceInstance.ContainingResource() rs := state.Resource(rAddr) diff --git a/internal/states/instance_object.go b/internal/states/instance_object.go index 3801d7b35383..8631377ae15f 100644 --- a/internal/states/instance_object.go +++ b/internal/states/instance_object.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/lang/format" "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/providers" ) // ResourceInstanceObject is the local representation of a specific remote @@ -27,6 +28,10 @@ type ResourceInstanceObject struct { // Terraform. Value cty.Value + // Identity is the object-typed value representing the identity of the remote + // object within Terraform. + Identity cty.Value + // Private is an opaque value set by the provider when this object was // last created or updated. Terraform Core does not use this value in // any way and it is not exposed anywhere in the user interface, so @@ -52,6 +57,24 @@ type ResourceInstanceObject struct { CreateBeforeDestroy bool } +// NewResourceInstanceObjectFromIR converts the receiving +// ImportedResource into a ResourceInstanceObject that has status ObjectReady. +// +// The returned object does not know its own resource type, so the caller must +// retain the ResourceType value from the source object if this information is +// needed. +// +// The returned object also has no dependency addresses, but the caller may +// freely modify the direct fields of the returned object without affecting +// the receiver. +func NewResourceInstanceObjectFromIR(ir providers.ImportedResource) *ResourceInstanceObject { + return &ResourceInstanceObject{ + Status: ObjectReady, + Value: ir.State, + Private: ir.Private, + } +} + // ObjectStatus represents the status of a RemoteObject. type ObjectStatus rune @@ -80,21 +103,20 @@ const ( ObjectPlanned ObjectStatus = 'P' ) -// Encode marshals the value within the receiver to produce a +// Encode marshals values within the receiver to produce a // ResourceInstanceObjectSrc ready to be written to a state file. // -// The given type must be the implied type of the resource type schema, and -// the given value must conform to it. It is important to pass the schema -// type and not the object's own type so that dynamically-typed attributes -// will be stored correctly. The caller must also provide the version number -// of the schema that the given type was derived from, which will be recorded -// in the source object so it can be used to detect when schema migration is -// required on read. +// The schema must contain the resource type body, and the given value must +// conform its implied type. The schema must also contain the version number +// of the schema, which will be recorded in the source object so it can be +// used to detect when schema migration is required on read. +// The schema may also contain an resource identity schema and version number, +// which will be used to encode the resource identity. // // The returned object may share internal references with the receiver and // so the caller must not mutate the receiver any further once once this // method is called. -func (o *ResourceInstanceObject) Encode(ty cty.Type, schemaVersion uint64) (*ResourceInstanceObjectSrc, error) { +func (o *ResourceInstanceObject) Encode(schema providers.Schema) (*ResourceInstanceObjectSrc, error) { // If it contains marks, remove these marks before traversing the // structure with UnknownAsNull, and save the PathValueMarks // so we can save them in state. @@ -114,11 +136,19 @@ func (o *ResourceInstanceObject) Encode(ty cty.Type, schemaVersion uint64) (*Res // and raise an error about that. val = cty.UnknownAsNull(val) - src, err := ctyjson.Marshal(val, ty) + src, err := ctyjson.Marshal(val, schema.Body.ImpliedType()) if err != nil { return nil, err } + var idJSON []byte + if !o.Identity.IsNull() && schema.Identity != nil { + idJSON, err = ctyjson.Marshal(o.Identity, schema.Identity.ImpliedType()) + if err != nil { + return nil, err + } + } + // Dependencies are collected and merged in an unordered format (using map // keys as a set), then later changed to a slice (in random ordering) to be // stored in state as an array. To avoid pointless thrashing of state in @@ -132,15 +162,18 @@ func (o *ResourceInstanceObject) Encode(ty cty.Type, schemaVersion uint64) (*Res sort.Slice(dependencies, func(i, j int) bool { return dependencies[i].String() < dependencies[j].String() }) return &ResourceInstanceObjectSrc{ - SchemaVersion: schemaVersion, - AttrsJSON: src, - AttrSensitivePaths: sensitivePaths, - Private: o.Private, - Status: o.Status, - Dependencies: dependencies, - CreateBeforeDestroy: o.CreateBeforeDestroy, + SchemaVersion: uint64(schema.Version), + AttrsJSON: src, + AttrSensitivePaths: sensitivePaths, + Private: o.Private, + Status: o.Status, + Dependencies: dependencies, + CreateBeforeDestroy: o.CreateBeforeDestroy, + IdentityJSON: idJSON, + IdentitySchemaVersion: uint64(schema.IdentityVersion), // The cached value must have all its marks since it bypasses decoding. - decodeValueCache: o.Value, + decodeValueCache: o.Value, + decodeIdentityCache: o.Identity, }, nil } diff --git a/internal/states/instance_object_src.go b/internal/states/instance_object_src.go index 7960524b66a8..a1088290106b 100644 --- a/internal/states/instance_object_src.go +++ b/internal/states/instance_object_src.go @@ -4,12 +4,15 @@ package states import ( + "fmt" + "github.com/zclconf/go-cty/cty" ctyjson "github.com/zclconf/go-cty/cty/json" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/hcl2shim" "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/providers" ) // ResourceInstanceObjectSrc is a not-fully-decoded version of @@ -40,6 +43,10 @@ type ResourceInstanceObjectSrc struct { // schema version should be recorded in the SchemaVersion field. AttrsJSON []byte + IdentitySchemaVersion uint64 + + IdentityJSON []byte + // AttrsFlat is a legacy form of attributes used in older state file // formats, and in the new state format for objects that haven't yet been // upgraded. This attribute is mutually exclusive with Attrs: for any @@ -66,21 +73,27 @@ type ResourceInstanceObjectSrc struct { // decodeValueCache stored the decoded value for repeated decodings. decodeValueCache cty.Value + // decodeIdentityCache stored the decoded identity for repeated decodings. + decodeIdentityCache cty.Value } // Decode unmarshals the raw representation of the object attributes. Pass the -// implied type of the corresponding resource type schema for correct operation. +// schema of the corresponding resource type for correct operation. // // Before calling Decode, the caller must check that the SchemaVersion field // exactly equals the version number of the schema whose implied type is being // passed, or else the result is undefined. // +// If the object has an identity, the schema must also contain a resource +// identity schema for the identity to be decoded. +// // The returned object may share internal references with the receiver and // so the caller must not mutate the receiver any further once once this // method is called. -func (os *ResourceInstanceObjectSrc) Decode(ty cty.Type) (*ResourceInstanceObject, error) { +func (os *ResourceInstanceObjectSrc) Decode(schema providers.Schema) (*ResourceInstanceObject, error) { var val cty.Value var err error + attrsTy := schema.Body.ImpliedType() switch { case os.decodeValueCache != cty.NilVal: @@ -88,21 +101,32 @@ func (os *ResourceInstanceObjectSrc) Decode(ty cty.Type) (*ResourceInstanceObjec case os.AttrsFlat != nil: // Legacy mode. We'll do our best to unpick this from the flatmap. - val, err = hcl2shim.HCL2ValueFromFlatmap(os.AttrsFlat, ty) + val, err = hcl2shim.HCL2ValueFromFlatmap(os.AttrsFlat, attrsTy) if err != nil { return nil, err } default: - val, err = ctyjson.Unmarshal(os.AttrsJSON, ty) + val, err = ctyjson.Unmarshal(os.AttrsJSON, attrsTy) val = marks.MarkPaths(val, marks.Sensitive, os.AttrSensitivePaths) if err != nil { return nil, err } } + var identity cty.Value + if os.decodeIdentityCache != cty.NilVal { + identity = os.decodeIdentityCache + } else if os.IdentityJSON != nil { + identity, err = ctyjson.Unmarshal(os.IdentityJSON, schema.Identity.ImpliedType()) + if err != nil { + return nil, fmt.Errorf("failed to decode identity schema: %s. This is most likely a bug in the Provider, providers must not change the identity schema without updating the identity schema version", err.Error()) + } + } + return &ResourceInstanceObject{ Value: val, + Identity: identity, Status: os.Status, Dependencies: os.Dependencies, Private: os.Private, @@ -131,3 +155,16 @@ func (os *ResourceInstanceObjectSrc) CompleteUpgrade(newAttrs cty.Value, newType new.SchemaVersion = newSchemaVersion return new, nil } + +func (os *ResourceInstanceObjectSrc) CompleteIdentityUpgrade(newAttrs cty.Value, schema providers.Schema) (*ResourceInstanceObjectSrc, error) { + new := os.DeepCopy() + + src, err := ctyjson.Marshal(newAttrs, schema.Identity.ImpliedType()) + if err != nil { + return nil, err + } + + new.IdentityJSON = src + new.IdentitySchemaVersion = uint64(schema.IdentityVersion) + return new, nil +} diff --git a/internal/states/instance_object_test.go b/internal/states/instance_object_test.go index 04818983a378..7b5a1a4bde8d 100644 --- a/internal/states/instance_object_test.go +++ b/internal/states/instance_object_test.go @@ -10,7 +10,9 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/providers" "github.com/zclconf/go-cty/cty" ) @@ -23,6 +25,27 @@ func TestResourceInstanceObject_encode(t *testing.T) { "sensitive_a": cty.StringVal("secret").Mark(marks.Sensitive), "sensitive_b": cty.StringVal("secret").Mark(marks.Sensitive), }) + schema := providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "foo": { + Type: cty.Bool, + }, + "obj": { + Type: cty.Object(map[string]cty.Type{ + "sensitive": cty.String, + }), + }, + "sensitive_a": { + Type: cty.String, + }, + "sensitive_b": { + Type: cty.String, + }, + }, + }, + Version: 0, + } // The in-memory order of resource dependencies is random, since they're an // unordered set. depsOne := []addrs.ConfigResource{ @@ -72,7 +95,7 @@ func TestResourceInstanceObject_encode(t *testing.T) { wg.Add(1) go func() { defer wg.Done() - rios, err := obj.Encode(value.Type(), 0) + rios, err := obj.Encode(schema) if err != nil { t.Errorf("unexpected error: %s", err) } @@ -108,12 +131,22 @@ func TestResourceInstanceObject_encodeInvalidMarks(t *testing.T) { // value in the state. "foo": cty.True.Mark("unsupported"), }) + schema := providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "foo": { + Type: cty.Bool, + }, + }, + }, + Version: 0, + } obj := &ResourceInstanceObject{ Value: value, Status: ObjectReady, } - _, err := obj.Encode(value.Type(), 0) + _, err := obj.Encode(schema) if err == nil { t.Fatalf("unexpected success; want error") } diff --git a/internal/states/remote/state_test.go b/internal/states/remote/state_test.go index cbb6a6219f07..8d207f7a355e 100644 --- a/internal/states/remote/state_test.go +++ b/internal/states/remote/state_test.go @@ -129,8 +129,9 @@ func TestStatePersist(t *testing.T) { "attributes_flat": map[string]interface{}{ "filename": "file.txt", }, - "schema_version": 0.0, - "sensitive_attributes": []interface{}{}, + "identity_schema_version": 0.0, + "schema_version": 0.0, + "sensitive_attributes": []interface{}{}, }, }, "mode": "managed", @@ -167,8 +168,9 @@ func TestStatePersist(t *testing.T) { "attributes_flat": map[string]interface{}{ "filename": "file.txt", }, - "schema_version": 0.0, - "sensitive_attributes": []interface{}{}, + "identity_schema_version": 0.0, + "schema_version": 0.0, + "sensitive_attributes": []interface{}{}, }, }, "mode": "managed", diff --git a/internal/states/state_deepcopy.go b/internal/states/state_deepcopy.go index f99828fea3b6..2c2d82c5636b 100644 --- a/internal/states/state_deepcopy.go +++ b/internal/states/state_deepcopy.go @@ -142,6 +142,12 @@ func (os *ResourceInstanceObjectSrc) DeepCopy() *ResourceInstanceObjectSrc { copy(attrsJSON, os.AttrsJSON) } + var identityJSON []byte + if os.IdentityJSON != nil { + identityJSON = make([]byte, len(os.IdentityJSON)) + copy(identityJSON, os.IdentityJSON) + } + var sensitiveAttrPaths []cty.Path if os.AttrSensitivePaths != nil { sensitiveAttrPaths = make([]cty.Path, len(os.AttrSensitivePaths)) @@ -163,15 +169,18 @@ func (os *ResourceInstanceObjectSrc) DeepCopy() *ResourceInstanceObjectSrc { } return &ResourceInstanceObjectSrc{ - Status: os.Status, - SchemaVersion: os.SchemaVersion, - Private: private, - AttrsFlat: attrsFlat, - AttrsJSON: attrsJSON, - AttrSensitivePaths: sensitiveAttrPaths, - Dependencies: dependencies, - CreateBeforeDestroy: os.CreateBeforeDestroy, - decodeValueCache: os.decodeValueCache, + Status: os.Status, + SchemaVersion: os.SchemaVersion, + Private: private, + AttrsFlat: attrsFlat, + AttrsJSON: attrsJSON, + AttrSensitivePaths: sensitiveAttrPaths, + Dependencies: dependencies, + CreateBeforeDestroy: os.CreateBeforeDestroy, + decodeValueCache: os.decodeValueCache, + IdentityJSON: identityJSON, + IdentitySchemaVersion: os.IdentitySchemaVersion, + decodeIdentityCache: os.decodeIdentityCache, } } diff --git a/internal/states/statefile/version4.go b/internal/states/statefile/version4.go index 6477e66cf24c..22b438c19b57 100644 --- a/internal/states/statefile/version4.go +++ b/internal/states/statefile/version4.go @@ -494,6 +494,8 @@ func appendInstanceObjectStateV4(rs *states.Resource, is *states.ResourceInstanc PrivateRaw: privateRaw, Dependencies: deps, CreateBeforeDestroy: obj.CreateBeforeDestroy, + IdentitySchemaVersion: obj.IdentitySchemaVersion, + IdentityRaw: obj.IdentityJSON, }), diags } @@ -702,6 +704,9 @@ type instanceObjectStateV4 struct { AttributesFlat map[string]string `json:"attributes_flat,omitempty"` AttributeSensitivePaths json.RawMessage `json:"sensitive_attributes,omitempty"` + IdentitySchemaVersion uint64 `json:"identity_schema_version"` + IdentityRaw json.RawMessage `json:"identity,omitempty"` + PrivateRaw []byte `json:"private,omitempty"` Dependencies []string `json:"dependencies,omitempty"` diff --git a/internal/terraform/context_apply2_test.go b/internal/terraform/context_apply2_test.go index 55328cacd59c..69987556dd2d 100644 --- a/internal/terraform/context_apply2_test.go +++ b/internal/terraform/context_apply2_test.go @@ -841,10 +841,20 @@ resource "test_resource" "c" { for name, attrs := range wantResourceAttrs { addr := mustResourceInstanceAddr(fmt.Sprintf("test_resource.%s", name)) r := state.ResourceInstance(addr) - rd, err := r.Current.Decode(cty.Object(map[string]cty.Type{ - "value": cty.String, - "output": cty.String, - })) + schema := providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + }, + "output": { + Type: cty.String, + }, + }, + }, + Version: 0, + } + rd, err := r.Current.Decode(schema) if err != nil { t.Fatalf("error decoding test_resource.a: %s", err) } @@ -902,10 +912,20 @@ resource "test_resource" "c" { for name, attrs := range wantResourceAttrs { addr := mustResourceInstanceAddr(fmt.Sprintf("test_resource.%s", name)) r := state.ResourceInstance(addr) - rd, err := r.Current.Decode(cty.Object(map[string]cty.Type{ - "value": cty.String, - "output": cty.String, - })) + schema := providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + }, + "output": { + Type: cty.String, + }, + }, + }, + Version: 0, + } + rd, err := r.Current.Decode(schema) if err != nil { t.Fatalf("error decoding test_resource.a: %s", err) } diff --git a/internal/terraform/context_apply_ephemeral_test.go b/internal/terraform/context_apply_ephemeral_test.go index 463835c029fd..cdd1553040f6 100644 --- a/internal/terraform/context_apply_ephemeral_test.go +++ b/internal/terraform/context_apply_ephemeral_test.go @@ -377,24 +377,26 @@ resource "ephem_write_only" "wo" { `, }) + schema := providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "normal": { + Type: cty.String, + Required: true, + }, + "write_only": { + Type: cty.String, + WriteOnly: true, + Required: true, + }, + }, + }, + Version: 0, + } ephem := &testing_provider.MockProvider{ GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ - "ephem_write_only": { - Body: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "normal": { - Type: cty.String, - Required: true, - }, - "write_only": { - Type: cty.String, - WriteOnly: true, - Required: true, - }, - }, - }, - }, + "ephem_write_only": schema, }, }, } @@ -457,10 +459,7 @@ resource "ephem_write_only" "wo" { t.Fatalf("Resource instance not found") } - attrs, err := resourceInstance.Current.Decode(cty.Object(map[string]cty.Type{ - "normal": cty.String, - "write_only": cty.String, - })) + attrs, err := resourceInstance.Current.Decode(schema) if err != nil { t.Fatalf("Failed to decode attributes: %v", err) } @@ -489,24 +488,25 @@ resource "ephem_write_only" "wo" { `, }) + schema := providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "normal": { + Type: cty.String, + Required: true, + }, + "write_only": { + Type: cty.String, + WriteOnly: true, + Required: true, + }, + }, + }, + } ephem := &testing_provider.MockProvider{ GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ - "ephem_write_only": { - Body: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "normal": { - Type: cty.String, - Required: true, - }, - "write_only": { - Type: cty.String, - WriteOnly: true, - Required: true, - }, - }, - }, - }, + "ephem_write_only": schema, }, }, } @@ -585,10 +585,7 @@ resource "ephem_write_only" "wo" { t.Fatalf("Resource instance not found") } - attrs, err := resourceInstance.Current.Decode(cty.Object(map[string]cty.Type{ - "normal": cty.String, - "write_only": cty.String, - })) + attrs, err := resourceInstance.Current.Decode(schema) if err != nil { t.Fatalf("Failed to decode attributes: %v", err) } @@ -617,24 +614,25 @@ resource "ephem_write_only" "wo" { `, }) + schema := providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "normal": { + Type: cty.String, + Required: true, + }, + "write_only": { + Type: cty.String, + WriteOnly: true, + Required: true, + }, + }, + }, + } ephem := &testing_provider.MockProvider{ GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ - "ephem_write_only": { - Body: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "normal": { - Type: cty.String, - Required: true, - }, - "write_only": { - Type: cty.String, - WriteOnly: true, - Required: true, - }, - }, - }, - }, + "ephem_write_only": schema, }, }, } @@ -724,10 +722,7 @@ resource "ephem_write_only" "wo" { t.Fatalf("Resource instance not found") } - attrs, err := resourceInstance.Current.Decode(cty.Object(map[string]cty.Type{ - "normal": cty.String, - "write_only": cty.String, - })) + attrs, err := resourceInstance.Current.Decode(schema) if err != nil { t.Fatalf("Failed to decode attributes: %v", err) } diff --git a/internal/terraform/context_apply_test.go b/internal/terraform/context_apply_test.go index e14753d4b9d7..aeeb0eb39ade 100644 --- a/internal/terraform/context_apply_test.go +++ b/internal/terraform/context_apply_test.go @@ -273,9 +273,9 @@ func TestContext2Apply_unstable(t *testing.T) { Type: "test_resource", Name: "foo", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - schema := p.GetProviderSchemaResponse.ResourceTypes["test_resource"].Body + schema := p.GetProviderSchemaResponse.ResourceTypes["test_resource"] rds := plan.Changes.ResourceInstance(addr) - rd, err := rds.Decode(schema.ImpliedType()) + rd, err := rds.Decode(schema.Body.ImpliedType()) if err != nil { t.Fatal(err) } @@ -295,7 +295,7 @@ func TestContext2Apply_unstable(t *testing.T) { t.Fatalf("wrong number of resources %d; want 1", len(mod.Resources)) } - rs, err := rss.Current.Decode(schema.ImpliedType()) + rs, err := rss.Current.Decode(schema) if err != nil { t.Fatalf("decode error: %v", err) } diff --git a/internal/terraform/context_plan.go b/internal/terraform/context_plan.go index 3eae86d47d60..e2104c266279 100644 --- a/internal/terraform/context_plan.go +++ b/internal/terraform/context_plan.go @@ -864,12 +864,12 @@ func (c *Context) deferredResources(config *configs.Config, deferrals []*plans.D for _, deferral := range deferrals { - schema, _ := schemas.ResourceTypeConfig( + schema := schemas.ResourceTypeConfig( deferral.Change.ProviderAddr.Provider, deferral.Change.Addr.Resource.Resource.Mode, deferral.Change.Addr.Resource.Resource.Type) - ty := schema.ImpliedType() + ty := schema.Body.ImpliedType() deferralSrc, err := deferral.Encode(ty) if err != nil { diags = diags.Append(tfdiags.Sourceless( @@ -1001,12 +1001,12 @@ func (c *Context) driftedResources(config *configs.Config, oldState, newState *s newIS := newState.ResourceInstance(addr) - schema, _ := schemas.ResourceTypeConfig( + schema := schemas.ResourceTypeConfig( provider, addr.Resource.Resource.Mode, addr.Resource.Resource.Type, ) - if schema == nil { + if schema.Body == nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Warning, "Missing resource schema from provider", @@ -1014,9 +1014,8 @@ func (c *Context) driftedResources(config *configs.Config, oldState, newState *s )) continue } - ty := schema.ImpliedType() - oldObj, err := oldIS.Current.Decode(ty) + oldObj, err := oldIS.Current.Decode(schema) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Warning, @@ -1028,7 +1027,7 @@ func (c *Context) driftedResources(config *configs.Config, oldState, newState *s var newObj *states.ResourceInstanceObject if newIS != nil && newIS.Current != nil { - newObj, err = newIS.Current.Decode(ty) + newObj, err = newIS.Current.Decode(schema) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Warning, @@ -1039,6 +1038,7 @@ func (c *Context) driftedResources(config *configs.Config, oldState, newState *s } } + ty := schema.Body.ImpliedType() var oldVal, newVal cty.Value oldVal = oldObj.Value if newObj != nil { diff --git a/internal/terraform/context_plan_identity_test.go b/internal/terraform/context_plan_identity_test.go new file mode 100644 index 000000000000..cafc3531fe33 --- /dev/null +++ b/internal/terraform/context_plan_identity_test.go @@ -0,0 +1,427 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/configs/hcl2shim" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestContext2Plan_resource_identity_refresh(t *testing.T) { + for name, tc := range map[string]struct { + StoredIdentitySchemaVersion uint64 + StoredIdentityJSON []byte + IdentitySchema providers.IdentitySchema + IdentityData cty.Value + ExpectedIdentity cty.Value + ExpectedError error + ExpectUpgradeResourceIdentityCalled bool + UpgradeResourceIdentityResponse providers.UpgradeResourceIdentityResponse + }{ + "no previous identity": { + IdentitySchema: providers.IdentitySchema{ + Version: 0, + Body: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + IdentityData: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + ExpectedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + }, + "identity version mismatch": { + StoredIdentitySchemaVersion: 1, + StoredIdentityJSON: []byte(`{"id": "foo"}`), + IdentitySchema: providers.IdentitySchema{ + Version: 0, + Body: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + IdentityData: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + ExpectedError: fmt.Errorf("Resource instance managed by newer provider version: The current state of aws_instance.web was created by a newer provider version than is currently selected. Upgrade the aws provider to work with this state."), + }, + "identity type mismatch": { + StoredIdentitySchemaVersion: 0, + StoredIdentityJSON: []byte(`{"arn": "foo"}`), + IdentitySchema: providers.IdentitySchema{ + Version: 0, + Body: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + IdentityData: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + ExpectedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + ExpectedError: fmt.Errorf("failed to decode identity schema: unsupported attribute \"arn\". This is most likely a bug in the Provider, providers must not change the identity schema without updating the identity schema version"), + }, + "identity upgrade succeeds": { + StoredIdentitySchemaVersion: 1, + StoredIdentityJSON: []byte(`{"arn": "foo"}`), + IdentitySchema: providers.IdentitySchema{ + Version: 2, + Body: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + IdentityData: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + UpgradeResourceIdentityResponse: providers.UpgradeResourceIdentityResponse{ + UpgradedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + }, + ExpectedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + ExpectUpgradeResourceIdentityCalled: true, + }, + "identity upgrade failed": { + StoredIdentitySchemaVersion: 1, + StoredIdentityJSON: []byte(`{"id": "foo"}`), + IdentitySchema: providers.IdentitySchema{ + Version: 2, + Body: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "arn": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + IdentityData: cty.ObjectVal(map[string]cty.Value{ + "arn": cty.StringVal("arn:foo"), + }), + UpgradeResourceIdentityResponse: providers.UpgradeResourceIdentityResponse{ + UpgradedIdentity: cty.NilVal, + Diagnostics: tfdiags.Diagnostics{ + tfdiags.Sourceless(tfdiags.Error, "failed to upgrade resource identity", "provider was unable to do so"), + }, + }, + ExpectedIdentity: cty.ObjectVal(map[string]cty.Value{ + "arn": cty.StringVal("arn:foo"), + }), + ExpectUpgradeResourceIdentityCalled: true, + ExpectedError: fmt.Errorf("failed to upgrade resource identity: provider was unable to do so"), + }, + "identity sent to provider differs from returned one": { + StoredIdentitySchemaVersion: 0, + StoredIdentityJSON: []byte(`{"id": "foo"}`), + IdentitySchema: providers.IdentitySchema{ + Version: 0, + Body: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + IdentityData: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("bar"), + }), + ExpectedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + ExpectedError: fmt.Errorf("Provider produced different identity: Provider \"registry.terraform.io/hashicorp/aws\" planned an different identity for aws_instance.web during refresh. \n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker."), + }, + "identity with unknowns": { + IdentitySchema: providers.IdentitySchema{ + Version: 0, + Body: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + IdentityData: cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + }), + ExpectedError: fmt.Errorf("Provider produced invalid identity: Provider \"registry.terraform.io/hashicorp/aws\" planned an identity with unknown values for aws_instance.web during refresh. \n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker."), + }, + + "identity with marks": { + IdentitySchema: providers.IdentitySchema{ + Version: 0, + Body: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + IdentityData: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("marked value").Mark(marks.Sensitive), + }), + ExpectedError: fmt.Errorf("Provider produced invalid identity: Provider \"registry.terraform.io/hashicorp/aws\" planned an identity with marks for aws_instance.web during refresh. \n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker."), + }, + } { + t.Run(name, func(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "refresh-basic") + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "aws_instance": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + "foo": { + Type: cty.String, + Optional: true, + Computed: true, + }, + }, + }, + }, + IdentityTypes: map[string]*configschema.Object{ + "aws_instance": tc.IdentitySchema.Body, + }, + IdentityTypeSchemaVersions: map[string]uint64{ + "aws_instance": uint64(tc.IdentitySchema.Version), + }, + }) + + state := states.NewState() + root := state.EnsureModule(addrs.RootModuleInstance) + + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("aws_instance.web").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"foo","foo":"bar"}`), + IdentitySchemaVersion: tc.StoredIdentitySchemaVersion, + IdentityJSON: tc.StoredIdentityJSON, + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), + ) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), + }, + }) + + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] + ty := schema.Body.ImpliedType() + readState, err := hcl2shim.HCL2ValueFromFlatmap(map[string]string{"id": "foo", "foo": "baz"}, ty) + if err != nil { + t.Fatal(err) + } + + p.ReadResourceResponse = &providers.ReadResourceResponse{ + NewState: readState, + Identity: tc.IdentityData, + } + + p.UpgradeResourceIdentityResponse = &tc.UpgradeResourceIdentityResponse + + s, diags := ctx.Plan(m, state, &PlanOpts{Mode: plans.RefreshOnlyMode}) + + // TODO: maybe move to comparing diagnostics instead + if tc.ExpectedError != nil { + if !diags.HasErrors() { + t.Fatal("expected error, got none") + } + if diags.Err().Error() != tc.ExpectedError.Error() { + t.Fatalf("unexpected error\nwant: %v\ngot: %v", tc.ExpectedError, diags.Err()) + } + + return + } else { + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + } + + if !p.ReadResourceCalled { + t.Fatal("ReadResource should be called") + } + + if tc.ExpectUpgradeResourceIdentityCalled && !p.UpgradeResourceIdentityCalled { + t.Fatal("UpgradeResourceIdentity should be called") + } + + mod := s.PriorState.RootModule() + fromState, err := mod.Resources["aws_instance.web"].Instances[addrs.NoKey].Current.Decode(schema) + if err != nil { + t.Fatal(err) + } + + newState, err := schema.Body.CoerceValue(fromState.Value) + if err != nil { + t.Fatal(err) + } + + if !cmp.Equal(readState, newState, valueComparer) { + t.Fatal(cmp.Diff(readState, newState, valueComparer, equateEmpty)) + } + + if tc.ExpectedIdentity.Equals(fromState.Identity).False() { + t.Fatalf("wrong identity\nwant: %s\ngot: %s", tc.ExpectedIdentity.GoString(), fromState.Identity.GoString()) + } + }) + } +} + +// This test validates if a resource identity that is deposed and will be destroyed +// can be refreshed with an identity during the plan. +func TestContext2Plan_resource_identity_refresh_destroy_deposed(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "refresh-basic") + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "aws_instance": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + "foo": { + Type: cty.String, + Optional: true, + Computed: true, + }, + }, + }, + }, + IdentityTypes: map[string]*configschema.Object{ + "aws_instance": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + IdentityTypeSchemaVersions: map[string]uint64{ + "aws_instance": 0, + }, + }) + + state := states.NewState() + root := state.EnsureModule(addrs.RootModuleInstance) + + deposedKey := states.DeposedKey("00000001") + root.SetResourceInstanceDeposed( + mustResourceInstanceAddr("aws_instance.web").Resource, + deposedKey, + &states.ResourceInstanceObjectSrc{ // no identity recorded + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"foo","foo":"bar"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), + ) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), + }, + }) + + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] + ty := schema.Body.ImpliedType() + readState, err := hcl2shim.HCL2ValueFromFlatmap(map[string]string{"id": "foo", "foo": "baz"}, ty) + if err != nil { + t.Fatal(err) + } + + p.ReadResourceResponse = &providers.ReadResourceResponse{ + NewState: readState, + Identity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + } + + s, diags := ctx.Plan(m, state, &PlanOpts{Mode: plans.RefreshOnlyMode}) + + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + + if !p.ReadResourceCalled { + t.Fatal("ReadResource should be called") + } + + mod := s.PriorState.RootModule() + fromState, err := mod.Resources["aws_instance.web"].Instances[addrs.NoKey].Deposed[deposedKey].Decode(schema) + if err != nil { + t.Fatal(err) + } + + newState, err := schema.Body.CoerceValue(fromState.Value) + if err != nil { + t.Fatal(err) + } + + if !cmp.Equal(readState, newState, valueComparer) { + t.Fatal(cmp.Diff(readState, newState, valueComparer, equateEmpty)) + } + expectedIdentity := cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }) + if expectedIdentity.Equals(fromState.Identity).False() { + t.Fatalf("wrong identity\nwant: %s\ngot: %s", expectedIdentity.GoString(), fromState.Identity.GoString()) + } + +} diff --git a/internal/terraform/context_refresh_test.go b/internal/terraform/context_refresh_test.go index 61275f400f84..bdad90ff2608 100644 --- a/internal/terraform/context_refresh_test.go +++ b/internal/terraform/context_refresh_test.go @@ -43,8 +43,8 @@ func TestContext2Refresh(t *testing.T) { }, }) - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Body - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] + ty := schema.Body.ImpliedType() readState, err := hcl2shim.HCL2ValueFromFlatmap(map[string]string{"id": "foo", "foo": "baz"}, ty) if err != nil { t.Fatal(err) @@ -64,12 +64,12 @@ func TestContext2Refresh(t *testing.T) { } mod := s.RootModule() - fromState, err := mod.Resources["aws_instance.web"].Instances[addrs.NoKey].Current.Decode(ty) + fromState, err := mod.Resources["aws_instance.web"].Instances[addrs.NoKey].Current.Decode(schema) if err != nil { t.Fatal(err) } - newState, err := schema.CoerceValue(fromState.Value) + newState, err := schema.Body.CoerceValue(fromState.Value) if err != nil { t.Fatal(err) } @@ -130,9 +130,7 @@ func TestContext2Refresh_dynamicAttr(t *testing.T) { }, }) - schema := p.GetProviderSchemaResponse.ResourceTypes["test_instance"].Body - ty := schema.ImpliedType() - + schema := p.GetProviderSchemaResponse.ResourceTypes["test_instance"] s, diags := ctx.Refresh(m, startingState, &PlanOpts{Mode: plans.NormalMode}) if diags.HasErrors() { t.Fatal(diags.Err()) @@ -143,7 +141,7 @@ func TestContext2Refresh_dynamicAttr(t *testing.T) { } mod := s.RootModule() - newState, err := mod.Resources["test_instance.foo"].Instances[addrs.NoKey].Current.Decode(ty) + newState, err := mod.Resources["test_instance.foo"].Instances[addrs.NoKey].Current.Decode(schema) if err != nil { t.Fatal(err) } @@ -807,10 +805,8 @@ func TestContext2Refresh_stateBasic(t *testing.T) { }, }) - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Body - ty := schema.ImpliedType() - - readStateVal, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{ + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] + readStateVal, err := schema.Body.CoerceValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("foo"), })) if err != nil { @@ -831,7 +827,7 @@ func TestContext2Refresh_stateBasic(t *testing.T) { } mod := s.RootModule() - newState, err := mod.Resources["aws_instance.web"].Instances[addrs.NoKey].Current.Decode(ty) + newState, err := mod.Resources["aws_instance.web"].Instances[addrs.NoKey].Current.Decode(schema) if err != nil { t.Fatal(err) } @@ -889,11 +885,13 @@ func TestContext2Refresh_dataCount(t *testing.T) { func TestContext2Refresh_dataState(t *testing.T) { m := testModule(t, "refresh-data-resource-basic") state := states.NewState() - schema := &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "inputs": { - Type: cty.Map(cty.String), - Optional: true, + schema := providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "inputs": { + Type: cty.Map(cty.String), + Optional: true, + }, }, }, } @@ -902,7 +900,7 @@ func TestContext2Refresh_dataState(t *testing.T) { p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{}, DataSources: map[string]*configschema.Block{ - "null_data_source": schema, + "null_data_source": schema.Body, }, }) @@ -934,7 +932,7 @@ func TestContext2Refresh_dataState(t *testing.T) { mod := s.RootModule() - newState, err := mod.Resources["data.null_data_source.testing"].Instances[addrs.NoKey].Current.Decode(schema.ImpliedType()) + newState, err := mod.Resources["data.null_data_source.testing"].Instances[addrs.NoKey].Current.Decode(schema) if err != nil { t.Fatal(err) } @@ -1065,22 +1063,24 @@ func TestContext2Refresh_unknownProvider(t *testing.T) { func TestContext2Refresh_vars(t *testing.T) { p := testProvider("aws") - schema := &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "ami": { - Type: cty.String, - Optional: true, - }, - "id": { - Type: cty.String, - Computed: true, + schema := providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "ami": { + Type: cty.String, + Optional: true, + }, + "id": { + Type: cty.String, + Computed: true, + }, }, }, } p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{}, - ResourceTypes: map[string]*configschema.Block{"aws_instance": schema}, + ResourceTypes: map[string]*configschema.Block{"aws_instance": schema.Body}, }) m := testModule(t, "refresh-vars") @@ -1094,7 +1094,7 @@ func TestContext2Refresh_vars(t *testing.T) { }, }) - readStateVal, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{ + readStateVal, err := schema.Body.CoerceValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("foo"), })) if err != nil { @@ -1122,7 +1122,7 @@ func TestContext2Refresh_vars(t *testing.T) { mod := s.RootModule() - newState, err := mod.Resources["aws_instance.web"].Instances[addrs.NoKey].Current.Decode(schema.ImpliedType()) + newState, err := mod.Resources["aws_instance.web"].Instances[addrs.NoKey].Current.Decode(schema) if err != nil { t.Fatal(err) } diff --git a/internal/terraform/eval_context_mock.go b/internal/terraform/eval_context_mock.go index bd2edf2031b7..48b0a41fef5a 100644 --- a/internal/terraform/eval_context_mock.go +++ b/internal/terraform/eval_context_mock.go @@ -58,6 +58,11 @@ type MockEvalContext struct { ProviderSchemaSchema providers.ProviderSchema ProviderSchemaError error + ResourceIdentitySchemasCalled bool + ResourceIdentitySchemasAddr addrs.AbsProviderConfig + ResourceIdentitySchemasSchemas providers.ResourceIdentitySchemas + ResourceIdentitySchemasError error + CloseProviderCalled bool CloseProviderAddr addrs.AbsProviderConfig CloseProviderProvider providers.Interface @@ -209,6 +214,12 @@ func (c *MockEvalContext) ProviderSchema(addr addrs.AbsProviderConfig) (provider return c.ProviderSchemaSchema, c.ProviderSchemaError } +func (c *MockEvalContext) ResourceIdentitySchemas(addr addrs.AbsProviderConfig) (providers.ResourceIdentitySchemas, error) { + c.ResourceIdentitySchemasCalled = true + c.ResourceIdentitySchemasAddr = addr + return c.ResourceIdentitySchemasSchemas, c.ProviderSchemaError +} + func (c *MockEvalContext) CloseProvider(addr addrs.AbsProviderConfig) error { c.CloseProviderCalled = true c.CloseProviderAddr = addr diff --git a/internal/terraform/eval_provider.go b/internal/terraform/eval_provider.go index 4592028c763c..cb3a26922cd9 100644 --- a/internal/terraform/eval_provider.go +++ b/internal/terraform/eval_provider.go @@ -58,5 +58,6 @@ func getProvider(ctx EvalContext, addr addrs.AbsProviderConfig) (providers.Inter if err != nil { return nil, providers.ProviderSchema{}, fmt.Errorf("failed to read schema for provider %s: %w", addr, err) } + return provider, schema, nil } diff --git a/internal/terraform/evaluate.go b/internal/terraform/evaluate.go index d1e22e12e845..bbb365f89c64 100644 --- a/internal/terraform/evaluate.go +++ b/internal/terraform/evaluate.go @@ -13,7 +13,6 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" - "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/didyoumean" "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/lang" @@ -21,6 +20,7 @@ import ( "github.com/hashicorp/terraform/internal/namedvals" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/plans/deferring" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/resources/ephemeral" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" @@ -556,7 +556,7 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc // We need to build an abs provider address, but we can use a default // instance since we're only interested in the schema. schema := d.getResourceSchema(addr, config.Provider) - if schema == nil { + if schema.Body == nil { // This shouldn't happen, since validation before we get here should've // taken care of it, but we'll show a reasonable error message anyway. diags = diags.Append(&hcl.Diagnostic{ @@ -567,7 +567,7 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc }) return cty.DynamicVal, diags } - ty := schema.ImpliedType() + ty := schema.Body.ImpliedType() if addr.Mode == addrs.EphemeralResourceMode { // FIXME: This does not yet work with deferrals, and it would be nice to @@ -695,7 +695,7 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc continue } - ios, err := is.Current.Decode(ty) + ios, err := is.Current.Decode(schema) if err != nil { // This shouldn't happen, since by the time we get here we // should have upgraded the state data already. @@ -896,13 +896,13 @@ func (d *evaluationStateData) getEphemeralResource(addr addrs.Resource, rng tfdi } } -func (d *evaluationStateData) getResourceSchema(addr addrs.Resource, providerAddr addrs.Provider) *configschema.Block { - schema, _, err := d.Evaluator.Plugins.ResourceTypeSchema(providerAddr, addr.Mode, addr.Type) +func (d *evaluationStateData) getResourceSchema(addr addrs.Resource, providerAddr addrs.Provider) providers.Schema { + schema, err := d.Evaluator.Plugins.ResourceTypeSchema(providerAddr, addr.Mode, addr.Type) if err != nil { // We have plently other codepaths that will detect and report // schema lookup errors before we'd reach this point, so we'll just // treat a failure here the same as having no schema. - return nil + return providers.Schema{} } return schema } diff --git a/internal/terraform/evaluate_valid.go b/internal/terraform/evaluate_valid.go index 4af1fa2b15d6..b33d17b52e8e 100644 --- a/internal/terraform/evaluate_valid.go +++ b/internal/terraform/evaluate_valid.go @@ -249,7 +249,7 @@ func staticValidateResourceReference(modCfg *configs.Config, addr addrs.Resource } providerFqn := modCfg.Module.ProviderForLocalConfig(cfg.ProviderConfigAddr()) - schema, _, err := plugins.ResourceTypeSchema(providerFqn, addr.Mode, addr.Type) + schema, err := plugins.ResourceTypeSchema(providerFqn, addr.Mode, addr.Type) if err != nil { // Prior validation should've taken care of a schema lookup error, // so we should never get here but we'll handle it here anyway for @@ -262,7 +262,7 @@ func staticValidateResourceReference(modCfg *configs.Config, addr addrs.Resource }) } - if schema == nil { + if schema.Body == nil { // Prior validation should've taken care of a resource block with an // unsupported type, so we should never get here but we'll handle it // here anyway for robustness. @@ -298,7 +298,7 @@ func staticValidateResourceReference(modCfg *configs.Config, addr addrs.Resource // If we got this far then we'll try to validate the remaining traversal // steps against our schema. - moreDiags := schema.StaticValidateTraversal(remain) + moreDiags := schema.Body.StaticValidateTraversal(remain) diags = diags.Append(moreDiags) return diags diff --git a/internal/terraform/node_resource_abstract.go b/internal/terraform/node_resource_abstract.go index ae0403267860..94701bb578b0 100644 --- a/internal/terraform/node_resource_abstract.go +++ b/internal/terraform/node_resource_abstract.go @@ -491,12 +491,12 @@ func (n *NodeAbstractResource) readResourceInstanceState(ctx EvalContext, addr a return nil, nil } - schema, currentVersion := (providerSchema).SchemaForResourceAddr(addr.Resource.ContainingResource()) - if schema == nil { + schema := providerSchema.SchemaForResourceAddr(addr.Resource.ContainingResource()) + if schema.Body == nil { // Shouldn't happen since we should've failed long ago if no schema is present return nil, diags.Append(fmt.Errorf("no schema available for %s while reading state; this is a bug in Terraform and should be reported", addr)) } - src, upgradeDiags := upgradeResourceState(addr, provider, src, schema, currentVersion) + src, upgradeDiags := upgradeResourceState(addr, provider, src, schema) if n.Config != nil { upgradeDiags = upgradeDiags.InConfigBody(n.Config.Config, addr.String()) } @@ -505,7 +505,13 @@ func (n *NodeAbstractResource) readResourceInstanceState(ctx EvalContext, addr a return nil, diags } - obj, err := src.Decode(schema.ImpliedType()) + src, upgradeDiags = upgradeResourceIdentity(addr, provider, src, schema) + diags = diags.Append(upgradeDiags) + if diags.HasErrors() { + return nil, diags + } + + obj, err := src.Decode(schema) if err != nil { diags = diags.Append(err) } @@ -536,14 +542,14 @@ func (n *NodeAbstractResource) readResourceInstanceStateDeposed(ctx EvalContext, return nil, diags } - schema, currentVersion := (providerSchema).SchemaForResourceAddr(addr.Resource.ContainingResource()) - if schema == nil { + schema := providerSchema.SchemaForResourceAddr(addr.Resource.ContainingResource()) + if schema.Body == nil { // Shouldn't happen since we should've failed long ago if no schema is present return nil, diags.Append(fmt.Errorf("no schema available for %s while reading state; this is a bug in Terraform and should be reported", addr)) } - src, upgradeDiags := upgradeResourceState(addr, provider, src, schema, currentVersion) + src, upgradeDiags := upgradeResourceState(addr, provider, src, schema) if n.Config != nil { upgradeDiags = upgradeDiags.InConfigBody(n.Config.Config, addr.String()) } @@ -556,7 +562,13 @@ func (n *NodeAbstractResource) readResourceInstanceStateDeposed(ctx EvalContext, return nil, diags } - obj, err := src.Decode(schema.ImpliedType()) + src, upgradeDiags = upgradeResourceIdentity(addr, provider, src, schema) + diags = diags.Append(upgradeDiags) + if diags.HasErrors() { + return nil, diags + } + + obj, err := src.Decode(schema) if err != nil { diags = diags.Append(err) } diff --git a/internal/terraform/node_resource_abstract_instance.go b/internal/terraform/node_resource_abstract_instance.go index 8796e5b3ee15..7b2f64975ecd 100644 --- a/internal/terraform/node_resource_abstract_instance.go +++ b/internal/terraform/node_resource_abstract_instance.go @@ -167,8 +167,8 @@ func (n *NodeAbstractResourceInstance) readDiff(ctx EvalContext, providerSchema changes := ctx.Changes() addr := n.ResourceInstanceAddr() - schema, _ := providerSchema.SchemaForResourceAddr(addr.Resource.Resource) - if schema == nil { + schema := providerSchema.SchemaForResourceAddr(addr.Resource.Resource) + if schema.Body == nil { // Should be caught during validation, so we don't bother with a pretty error here return nil, fmt.Errorf("provider does not support resource type %q", addr.Resource.Resource.Type) } @@ -341,15 +341,15 @@ func (n *NodeAbstractResourceInstance) writeResourceInstanceStateImpl(ctx EvalCo log.Printf("[TRACE] %s: writing state object for %s", logFuncName, absAddr) - schema, currentVersion := providerSchema.SchemaForResourceAddr(absAddr.ContainingResource().Resource) - if schema == nil { + schema := providerSchema.SchemaForResourceAddr(absAddr.ContainingResource().Resource) + if schema.Body == nil { // It shouldn't be possible to get this far in any real scenario // without a schema, but we might end up here in contrived tests that // fail to set up their world properly. return fmt.Errorf("failed to encode %s in state: no resource type schema available", absAddr) } - src, err := obj.Encode(schema.ImpliedType(), currentVersion) + src, err := obj.Encode(schema) if err != nil { return fmt.Errorf("failed to encode %s in state: %s", absAddr, err) } @@ -596,8 +596,8 @@ func (n *NodeAbstractResourceInstance) refresh(ctx EvalContext, deposedKey state return state, deferred, diags } - schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.Resource.ContainingResource()) - if schema == nil { + schema := providerSchema.SchemaForResourceAddr(n.Addr.Resource.ContainingResource()) + 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 %q", n.Addr.Resource.Resource.Type)) return state, deferred, diags @@ -630,6 +630,7 @@ func (n *NodeAbstractResourceInstance) refresh(ctx EvalContext, deposedKey state // to the provider so we'll just return whatever was in state. resp = providers.ReadResourceResponse{ NewState: priorVal, + Identity: state.Identity, } } else { resp = provider.ReadResource(providers.ReadResourceRequest{ @@ -679,7 +680,7 @@ func (n *NodeAbstractResourceInstance) refresh(ctx EvalContext, deposedKey state )) } - for _, err := range resp.NewState.Type().TestConformance(schema.ImpliedType()) { + for _, err := range resp.NewState.Type().TestConformance(schema.Body.ImpliedType()) { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Provider produced invalid object", @@ -703,7 +704,7 @@ func (n *NodeAbstractResourceInstance) refresh(ctx EvalContext, deposedKey state ) }, resp.NewState, - schema, + schema.Body, ) diags = diags.Append(writeOnlyDiags) @@ -711,7 +712,12 @@ func (n *NodeAbstractResourceInstance) refresh(ctx EvalContext, deposedKey state return state, deferred, diags } - newState := objchange.NormalizeObjectFromLegacySDK(resp.NewState, schema) + diags = diags.Append(n.validateIdentity(state, resp.Identity, false)) + if diags.HasErrors() { + return state, deferred, diags + } + + newState := objchange.NormalizeObjectFromLegacySDK(resp.NewState, schema.Body) if !newState.RawEquals(resp.NewState) { // We had to fix up this object in some way, and we still need to // accept any changes for compatibility, so all we can do is log a @@ -722,6 +728,7 @@ func (n *NodeAbstractResourceInstance) refresh(ctx EvalContext, deposedKey state ret := state.DeepCopy() ret.Value = newState ret.Private = resp.Private + ret.Identity = resp.Identity // Call post-refresh hook diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) { @@ -737,7 +744,7 @@ func (n *NodeAbstractResourceInstance) refresh(ctx EvalContext, deposedKey state // the prior state. New marks may appear when the prior state was from an // import operation, or if the provider added new marks to the schema. ret.Value = ret.Value.MarkWithPaths(priorMarks) - if moreSensitivePaths := schema.SensitivePaths(ret.Value, nil); len(moreSensitivePaths) != 0 { + if moreSensitivePaths := schema.Body.SensitivePaths(ret.Value, nil); len(moreSensitivePaths) != 0 { ret.Value = marks.MarkPaths(ret.Value, marks.Sensitive, moreSensitivePaths) } @@ -763,8 +770,8 @@ func (n *NodeAbstractResourceInstance) plan( return nil, nil, deferred, keyData, diags.Append(err) } - schema, _ := providerSchema.SchemaForResourceAddr(resource) - if schema == nil { + schema := providerSchema.SchemaForResourceAddr(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 %q", resource.Type)) return nil, nil, deferred, keyData, diags @@ -819,10 +826,10 @@ func (n *NodeAbstractResourceInstance) plan( return plannedChange, currentState.DeepCopy(), deferred, keyData, diags } - origConfigVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema, nil, keyData) + origConfigVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema.Body, nil, keyData) diags = diags.Append(configDiags) diags = diags.Append( - validateResourceForbiddenEphemeralValues(ctx, origConfigVal, schema).InConfigBody(n.Config.Config, n.Addr.String()), + validateResourceForbiddenEphemeralValues(ctx, origConfigVal, schema.Body).InConfigBody(n.Config.Config, n.Addr.String()), ) if diags.HasErrors() { return nil, nil, deferred, keyData, diags @@ -848,10 +855,10 @@ func (n *NodeAbstractResourceInstance) plan( // result as if the provider had marked at least one argument // change as "requires replacement". priorValTainted = currentState.Value - priorVal = cty.NullVal(schema.ImpliedType()) + priorVal = cty.NullVal(schema.Body.ImpliedType()) } } else { - priorVal = cty.NullVal(schema.ImpliedType()) + priorVal = cty.NullVal(schema.Body.ImpliedType()) } log.Printf("[TRACE] Re-validating config for %q", n.Addr) @@ -885,7 +892,7 @@ func (n *NodeAbstractResourceInstance) plan( // starting values. // Here we operate on the marked values, so as to revert any changes to the // marks as well as the value. - configValIgnored, ignoreChangeDiags := n.processIgnoreChanges(priorVal, origConfigVal, schema) + configValIgnored, ignoreChangeDiags := n.processIgnoreChanges(priorVal, origConfigVal, schema.Body) diags = diags.Append(ignoreChangeDiags) if ignoreChangeDiags.HasErrors() { return nil, nil, deferred, keyData, diags @@ -897,7 +904,7 @@ func (n *NodeAbstractResourceInstance) plan( unmarkedConfigVal, unmarkedPaths := configValIgnored.UnmarkDeepWithPaths() unmarkedPriorVal, _ := priorVal.UnmarkDeepWithPaths() - proposedNewVal := objchange.ProposedNew(schema, unmarkedPriorVal, unmarkedConfigVal) + proposedNewVal := objchange.ProposedNew(schema.Body, unmarkedPriorVal, unmarkedConfigVal) // Call pre-diff hook diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) { @@ -919,7 +926,7 @@ func (n *NodeAbstractResourceInstance) plan( Value: n.override.Values, Range: n.override.Range, ComputedAsUnknown: !n.override.UseForPlan, - }, schema) + }, schema.Body) resp = providers.PlanResourceChangeResponse{ PlannedState: override, Diagnostics: overrideDiags, @@ -980,7 +987,7 @@ func (n *NodeAbstractResourceInstance) plan( ) }, plannedNewVal, - schema, + schema.Body, ) diags = diags.Append(writeOnlyDiags) @@ -992,7 +999,7 @@ func (n *NodeAbstractResourceInstance) plan( // here, since that allows the provider to do special logic like a // DiffSuppressFunc, but we still require that the provider produces // a value whose type conforms to the schema. - for _, err := range plannedNewVal.Type().TestConformance(schema.ImpliedType()) { + for _, err := range plannedNewVal.Type().TestConformance(schema.Body.ImpliedType()) { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Provider produced invalid plan", @@ -1007,7 +1014,7 @@ func (n *NodeAbstractResourceInstance) plan( return nil, nil, deferred, keyData, diags } - if errs := objchange.AssertPlanValid(schema, unmarkedPriorVal, unmarkedConfigVal, plannedNewVal); len(errs) > 0 { + if errs := objchange.AssertPlanValid(schema.Body, unmarkedPriorVal, unmarkedConfigVal, plannedNewVal); len(errs) > 0 { if resp.LegacyTypeSystem { // The shimming of the old type system in the legacy SDK is not precise // enough to pass this consistency check, so we'll give it a pass here, @@ -1069,11 +1076,11 @@ func (n *NodeAbstractResourceInstance) plan( unmarkedPaths = marks.RemoveAll(unmarkedPaths, marks.Ephemeral) plannedNewVal = plannedNewVal.MarkWithPaths(unmarkedPaths) - if sensitivePaths := schema.SensitivePaths(plannedNewVal, nil); len(sensitivePaths) != 0 { + if sensitivePaths := schema.Body.SensitivePaths(plannedNewVal, nil); len(sensitivePaths) != 0 { plannedNewVal = marks.MarkPaths(plannedNewVal, marks.Sensitive, sensitivePaths) } - writeOnlyPaths := schema.WriteOnlyPaths(plannedNewVal, nil) + writeOnlyPaths := schema.Body.WriteOnlyPaths(plannedNewVal, nil) reqRep, reqRepDiags := getRequiredReplaces(unmarkedPriorVal, unmarkedPlannedNewVal, writeOnlyPaths, resp.RequiresReplace, n.ResolvedProvider.Provider, n.Addr) diags = diags.Append(reqRepDiags) @@ -1096,7 +1103,7 @@ func (n *NodeAbstractResourceInstance) plan( // The resulting change should show any computed attributes changing // from known prior values to unknown values, unless the provider is // able to predict new values for any of these computed attributes. - nullPriorVal := cty.NullVal(schema.ImpliedType()) + nullPriorVal := cty.NullVal(schema.Body.ImpliedType()) // Since there is no prior state to compare after replacement, we need // a new unmarked config from our original with no ignored values. @@ -1106,7 +1113,7 @@ func (n *NodeAbstractResourceInstance) plan( } // create a new proposed value from the null state and the config - proposedNewVal = objchange.ProposedNew(schema, nullPriorVal, unmarkedConfigVal) + proposedNewVal = objchange.ProposedNew(schema.Body, nullPriorVal, unmarkedConfigVal) if n.override != nil { // In this case, we are always creating the resource so we don't @@ -1115,7 +1122,7 @@ func (n *NodeAbstractResourceInstance) plan( Value: n.override.Values, Range: n.override.Range, ComputedAsUnknown: !n.override.UseForPlan, - }, schema) + }, schema.Body) resp = providers.PlanResourceChangeResponse{ PlannedState: override, Diagnostics: overrideDiags, @@ -1158,7 +1165,7 @@ func (n *NodeAbstractResourceInstance) plan( plannedNewVal = plannedNewVal.MarkWithPaths(unmarkedPaths) } - for _, err := range plannedNewVal.Type().TestConformance(schema.ImpliedType()) { + for _, err := range plannedNewVal.Type().TestConformance(schema.Body.ImpliedType()) { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Provider produced invalid plan", @@ -1182,7 +1189,7 @@ func (n *NodeAbstractResourceInstance) plan( ) }, plannedNewVal, - schema, + schema.Body, ) diags = diags.Append(writeOnlyDiags) @@ -1544,8 +1551,8 @@ func (n *NodeAbstractResourceInstance) readDataSource(ctx EvalContext, configVal if diags.HasErrors() { return newVal, deferred, diags } - schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource().Resource) - if schema == nil { + schema := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource().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 %q does not support data source %q", n.ResolvedProvider, n.Addr.ContainingResource().Resource.Type)) return newVal, deferred, diags @@ -1590,7 +1597,7 @@ func (n *NodeAbstractResourceInstance) readDataSource(ctx EvalContext, configVal Value: n.override.Values, Range: n.override.Range, ComputedAsUnknown: false, - }, schema) + }, schema.Body) resp = providers.ReadDataSourceResponse{ State: override, Diagnostics: overrideDiags, @@ -1621,12 +1628,12 @@ func (n *NodeAbstractResourceInstance) readDataSource(ctx EvalContext, configVal if newVal == cty.NilVal { // This can happen with incompletely-configured mocks. We'll allow it // and treat it as an alias for a properly-typed null value. - newVal = cty.NullVal(schema.ImpliedType()) + newVal = cty.NullVal(schema.Body.ImpliedType()) } // We don't want to run the checks if the data source read is deferred if deferred == nil { - for _, err := range newVal.Type().TestConformance(schema.ImpliedType()) { + for _, err := range newVal.Type().TestConformance(schema.Body.ImpliedType()) { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Provider produced invalid object", @@ -1671,7 +1678,7 @@ func (n *NodeAbstractResourceInstance) readDataSource(ctx EvalContext, configVal } } newVal = newVal.MarkWithPaths(pvm) - if sensitivePaths := schema.SensitivePaths(newVal, nil); len(sensitivePaths) != 0 { + if sensitivePaths := schema.Body.SensitivePaths(newVal, nil); len(sensitivePaths) != 0 { newVal = marks.MarkPaths(newVal, marks.Sensitive, sensitivePaths) } @@ -1750,14 +1757,14 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, checkRule } config := *n.Config - schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource().Resource) - if schema == nil { + schema := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource().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 %q does not support data source %q", n.ResolvedProvider, n.Addr.ContainingResource().Resource.Type)) return nil, nil, deferred, keyData, diags } - objTy := schema.ImpliedType() + objTy := schema.Body.ImpliedType() priorVal := cty.NullVal(objTy) forEach, _, _ := evaluateForEachExpression(config.ForEach, ctx, false) @@ -1775,10 +1782,10 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, checkRule } var configDiags tfdiags.Diagnostics - configVal, _, configDiags = ctx.EvaluateBlock(config.Config, schema, nil, keyData) + configVal, _, configDiags = ctx.EvaluateBlock(config.Config, schema.Body, nil, keyData) diags = diags.Append(configDiags) diags = diags.Append( - validateResourceForbiddenEphemeralValues(ctx, configVal, schema).InConfigBody(n.Config.Config, n.Addr.String()), + validateResourceForbiddenEphemeralValues(ctx, configVal, schema.Body).InConfigBody(n.Config.Config, n.Addr.String()), ) if diags.HasErrors() { return nil, nil, deferred, keyData, diags @@ -1842,13 +1849,13 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, checkRule reason = plans.ResourceInstanceReadBecauseDependencyPending } - proposedNewVal := objchange.PlannedDataResourceObject(schema, unmarkedConfigVal) + proposedNewVal := objchange.PlannedDataResourceObject(schema.Body, unmarkedConfigVal) // even though we are only returning the config value because we can't // yet read the data source, we need to incorporate the schema marks so // that downstream consumers can detect them when planning. proposedNewVal = proposedNewVal.MarkWithPaths(unmarkedPaths) - if sensitivePaths := schema.SensitivePaths(proposedNewVal, nil); len(sensitivePaths) != 0 { + if sensitivePaths := schema.Body.SensitivePaths(proposedNewVal, nil); len(sensitivePaths) != 0 { proposedNewVal = marks.MarkPaths(proposedNewVal, marks.Sensitive, sensitivePaths) } @@ -1917,13 +1924,13 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, checkRule if readDiags.HasErrors() { // If we had errors, then we can cover that up by marking the new // state as unknown. - newVal = objchange.PlannedDataResourceObject(schema, unmarkedConfigVal) + newVal = objchange.PlannedDataResourceObject(schema.Body, unmarkedConfigVal) // not only do we want to ensure this synthetic value has the marks, // but since this is the value being returned from the data source // we need to ensure the schema marks are added as well. newVal = newVal.MarkWithPaths(unmarkedPaths) - if sensitivePaths := schema.SensitivePaths(newVal, nil); len(sensitivePaths) != 0 { + if sensitivePaths := schema.Body.SensitivePaths(newVal, nil); len(sensitivePaths) != 0 { newVal = marks.MarkPaths(newVal, marks.Sensitive, sensitivePaths) } @@ -2080,8 +2087,8 @@ func (n *NodeAbstractResourceInstance) applyDataSource(ctx EvalContext, planned } config := *n.Config - schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource().Resource) - if schema == nil { + schema := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource().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 %q does not support data source %q", n.ResolvedProvider, n.Addr.ContainingResource().Resource.Type)) return nil, keyData, diags @@ -2111,7 +2118,7 @@ func (n *NodeAbstractResourceInstance) applyDataSource(ctx EvalContext, planned return nil, keyData, diags } - configVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema, nil, keyData) + configVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema.Body, nil, keyData) diags = diags.Append(configDiags) if configDiags.HasErrors() { return nil, keyData, diags @@ -2477,8 +2484,8 @@ func (n *NodeAbstractResourceInstance) apply( if err != nil { return nil, diags.Append(err) } - schema, _ := providerSchema.SchemaForResourceType(n.Addr.Resource.Resource.Mode, n.Addr.Resource.Resource.Type) - if schema == nil { + schema := providerSchema.SchemaForResourceType(n.Addr.Resource.Resource.Mode, 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 %q", n.Addr.Resource.Resource.Type)) return nil, diags @@ -2489,7 +2496,7 @@ func (n *NodeAbstractResourceInstance) apply( configVal := cty.NullVal(cty.DynamicPseudoType) if applyConfig != nil { var configDiags tfdiags.Diagnostics - configVal, _, configDiags = ctx.EvaluateBlock(applyConfig.Config, schema, nil, keyData) + configVal, _, configDiags = ctx.EvaluateBlock(applyConfig.Config, schema.Body, nil, keyData) diags = diags.Append(configDiags) if configDiags.HasErrors() { return nil, diags @@ -2564,7 +2571,7 @@ func (n *NodeAbstractResourceInstance) apply( Value: n.override.Values, Range: n.override.Range, ComputedAsUnknown: false, - }, schema) + }, schema.Body) resp = providers.ApplyResourceChangeResponse{ NewState: override, Diagnostics: overrideDiags, @@ -2608,7 +2615,7 @@ func (n *NodeAbstractResourceInstance) apply( // we were trying to execute a delete, because the provider in this case // probably left the newVal unset intending it to be interpreted as "null". if change.After.IsNull() { - newVal = cty.NullVal(schema.ImpliedType()) + newVal = cty.NullVal(schema.Body.ImpliedType()) } if !diags.HasErrors() { @@ -2624,7 +2631,7 @@ func (n *NodeAbstractResourceInstance) apply( } var conformDiags tfdiags.Diagnostics - for _, err := range newVal.Type().TestConformance(schema.ImpliedType()) { + for _, err := range newVal.Type().TestConformance(schema.Body.ImpliedType()) { conformDiags = conformDiags.Append(tfdiags.Sourceless( tfdiags.Error, "Provider produced invalid object", @@ -2652,7 +2659,7 @@ func (n *NodeAbstractResourceInstance) apply( ) }, newVal, - schema, + schema.Body, ) diags = diags.Append(writeOnlyDiags) @@ -2705,7 +2712,7 @@ func (n *NodeAbstractResourceInstance) apply( // won't be included in afterPaths, which are only what was read from the // After plan value. newVal = newVal.MarkWithPaths(afterPaths) - if sensitivePaths := schema.SensitivePaths(newVal, nil); len(sensitivePaths) != 0 { + if sensitivePaths := schema.Body.SensitivePaths(newVal, nil); len(sensitivePaths) != 0 { newVal = marks.MarkPaths(newVal, marks.Sensitive, sensitivePaths) } @@ -2719,7 +2726,7 @@ func (n *NodeAbstractResourceInstance) apply( // a pass since the other errors are usually the explanation for // this one and so it's more helpful to let the user focus on the // root cause rather than distract with this extra problem. - if errs := objchange.AssertObjectCompatible(schema, change.After, newVal); len(errs) > 0 { + if errs := objchange.AssertObjectCompatible(schema.Body, change.After, newVal); len(errs) > 0 { if resp.LegacyTypeSystem { // The shimming of the old type system in the legacy SDK is not precise // enough to pass this consistency check, so we'll give it a pass here, @@ -2831,6 +2838,47 @@ func (n *NodeAbstractResourceInstance) prevRunAddr(ctx EvalContext) addrs.AbsRes return resourceInstancePrevRunAddr(ctx, n.Addr) } +func (n *NodeAbstractResourceInstance) validateIdentity(state *states.ResourceInstanceObject, newIdentity cty.Value, isAllowedToChange bool) (diags tfdiags.Diagnostics) { + + // Identities can not contain unknown values + if !newIdentity.IsWhollyKnown() { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Provider produced invalid identity", + fmt.Sprintf( + "Provider %q planned an identity with unknown values for %s during refresh. \n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", + n.ResolvedProvider.Provider, n.Addr, + ), + )) + } + + // Identities can not contain marks + if _, marks := newIdentity.UnmarkDeep(); len(marks) > 0 { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Provider produced invalid identity", + fmt.Sprintf( + "Provider %q planned an identity with marks for %s during refresh. \n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", + n.ResolvedProvider.Provider, n.Addr, + ), + )) + } + + // Identities can not change (except if they are re-created or initially recorded) + if !isAllowedToChange && !state.Identity.IsNull() && state.Identity.Equals(newIdentity).False() { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Provider produced different identity", + fmt.Sprintf( + "Provider %q planned an different identity for %s during refresh. \n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", + n.ResolvedProvider.Provider, n.Addr, + ), + )) + } + + return diags +} + func resourceInstancePrevRunAddr(ctx EvalContext, currentAddr addrs.AbsResourceInstance) addrs.AbsResourceInstance { table := ctx.MoveResults() return table.OldAddr(currentAddr) diff --git a/internal/terraform/node_resource_apply_instance.go b/internal/terraform/node_resource_apply_instance.go index 552807646c96..b5670f971e3c 100644 --- a/internal/terraform/node_resource_apply_instance.go +++ b/internal/terraform/node_resource_apply_instance.go @@ -407,8 +407,8 @@ func (n *NodeApplyableResourceInstance) checkPlannedChange(ctx EvalContext, plan var diags tfdiags.Diagnostics addr := n.ResourceInstanceAddr().Resource - schema, _ := providerSchema.SchemaForResourceAddr(addr.ContainingResource()) - if schema == nil { + schema := providerSchema.SchemaForResourceAddr(addr.ContainingResource()) + 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 %q", addr.Resource.Type)) return diags @@ -450,7 +450,7 @@ func (n *NodeApplyableResourceInstance) checkPlannedChange(ctx EvalContext, plan } } - errs := objchange.AssertObjectCompatible(schema, plannedChange.After, actualChange.After) + errs := objchange.AssertObjectCompatible(schema.Body, plannedChange.After, actualChange.After) for _, err := range errs { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, diff --git a/internal/terraform/node_resource_destroy_deposed.go b/internal/terraform/node_resource_destroy_deposed.go index ce4ce490066f..2fdf6fccfa04 100644 --- a/internal/terraform/node_resource_destroy_deposed.go +++ b/internal/terraform/node_resource_destroy_deposed.go @@ -447,14 +447,15 @@ func (n *NodeDestroyDeposedResourceInstanceObject) writeResourceInstanceState(ct return err } - schema, currentVersion := providerSchema.SchemaForResourceAddr(absAddr.ContainingResource().Resource) - if schema == nil { + schema := providerSchema.SchemaForResourceAddr(absAddr.ContainingResource().Resource) + if schema.Body == nil { // It shouldn't be possible to get this far in any real scenario // without a schema, but we might end up here in contrived tests that // fail to set up their world properly. return fmt.Errorf("failed to encode %s in state: no resource type schema available", absAddr) } - src, err := obj.Encode(schema.ImpliedType(), currentVersion) + + src, err := obj.Encode(schema) if err != nil { return fmt.Errorf("failed to encode %s in state: %s", absAddr, err) } diff --git a/internal/terraform/node_resource_ephemeral.go b/internal/terraform/node_resource_ephemeral.go index 3453f77dc86a..79103269788d 100644 --- a/internal/terraform/node_resource_ephemeral.go +++ b/internal/terraform/node_resource_ephemeral.go @@ -40,8 +40,8 @@ func ephemeralResourceOpen(ctx EvalContext, inp ephemeralResourceInput) (*provid } config := inp.config - schema, _ := providerSchema.SchemaForResourceAddr(inp.addr.ContainingResource().Resource) - if schema == nil { + schema := providerSchema.SchemaForResourceAddr(inp.addr.ContainingResource().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 %q does not support ephemeral resource %q", @@ -71,7 +71,7 @@ func ephemeralResourceOpen(ctx EvalContext, inp ephemeralResourceInput) (*provid return nil, diags // failed preconditions prevent further evaluation } - configVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema, nil, keyData) + configVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema.Body, nil, keyData) diags = diags.Append(configDiags) if diags.HasErrors() { return nil, diags @@ -84,7 +84,7 @@ func ephemeralResourceOpen(ctx EvalContext, inp ephemeralResourceInput) (*provid // We don't know what the result will be, but we need to keep the // configured attributes for consistent evaluation. We can use the same // technique we used for data sources to create the plan-time value. - unknownResult := objchange.PlannedDataResourceObject(schema, unmarkedConfigVal) + unknownResult := objchange.PlannedDataResourceObject(schema.Body, unmarkedConfigVal) // add back any configured marks unknownResult = unknownResult.MarkWithPaths(configMarks) // and mark the entire value as ephemeral, since it's coming from an ephemeral context. @@ -135,7 +135,7 @@ func ephemeralResourceOpen(ctx EvalContext, inp ephemeralResourceInput) (*provid } resultVal := resp.Result.MarkWithPaths(configMarks) - errs := objchange.AssertPlanValid(schema, cty.NullVal(schema.ImpliedType()), configVal, resultVal) + errs := objchange.AssertPlanValid(schema.Body, cty.NullVal(schema.Body.ImpliedType()), configVal, resultVal) for _, err := range errs { diags = diags.Append(tfdiags.AttributeValue( tfdiags.Error, diff --git a/internal/terraform/node_resource_import.go b/internal/terraform/node_resource_import.go index 64ec84d1cf9d..82a269cb8436 100644 --- a/internal/terraform/node_resource_import.go +++ b/internal/terraform/node_resource_import.go @@ -81,8 +81,8 @@ func (n *graphNodeImportState) Execute(ctx EvalContext, op walkOperation) (diags return diags } - schema, _ := providerSchema.SchemaForResourceType(n.Addr.Resource.Resource.Mode, n.Addr.Resource.Resource.Type) - if schema == nil { + schema := providerSchema.SchemaForResourceType(n.Addr.Resource.Resource.Mode, 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 %q", n.Addr.Resource.Resource.Type)) return diags @@ -125,7 +125,7 @@ func (n *graphNodeImportState) Execute(ctx EvalContext, op walkOperation) (diags ) }, imported.State, - schema, + schema.Body, ) diags = diags.Append(writeOnlyDiags) } @@ -250,7 +250,7 @@ func (n *graphNodeImportStateSub) Execute(ctx EvalContext, op walkOperation) (di return diags } - state := n.State.AsInstanceObject() + state := states.NewResourceInstanceObjectFromIR(n.State) // Refresh riNode := &NodeAbstractResourceInstance{ diff --git a/internal/terraform/node_resource_plan_instance.go b/internal/terraform/node_resource_plan_instance.go index 43a903e5b975..e2f2cd680a68 100644 --- a/internal/terraform/node_resource_plan_instance.go +++ b/internal/terraform/node_resource_plan_instance.go @@ -587,8 +587,8 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs. return nil, deferred, diags } - schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.Resource.Resource) - if schema == nil { + 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 nil, deferred, diags @@ -615,7 +615,7 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs. forEach, _, _ := evaluateForEachExpression(n.Config.ForEach, ctx, false) keyData := EvalDataForInstanceKey(n.ResourceInstanceAddr().Resource.Key, forEach) - configVal, _, configDiags := ctx.EvaluateBlock(n.Config.Config, schema, nil, keyData) + configVal, _, configDiags := ctx.EvaluateBlock(n.Config.Config, schema.Body, nil, keyData) if configDiags.HasErrors() { // We have an overridden resource so we're definitely in a test and // the users config is not valid. So give up and just report the @@ -638,7 +638,7 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs. Value: n.override.Values, Range: n.override.Range, ComputedAsUnknown: false, - }, schema) + }, schema.Body) resp = providers.ImportResourceStateResponse{ ImportedResources: []providers.ImportedResource{ { @@ -699,12 +699,12 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs. // state we're going to import. state := providers.ImportedResource{ TypeName: addr.Resource.Resource.Type, - State: cty.NullVal(schema.ImpliedType()), + State: cty.NullVal(schema.Body.ImpliedType()), } // We skip the read and further validation since we make up the state // of the imported resource anyways. - return state.AsInstanceObject(), deferred, diags + return states.NewResourceInstanceObjectFromIR(state), deferred, diags } for _, obj := range imported { @@ -735,7 +735,7 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs. ) }, imported[0].State, - schema, + schema.Body, ) diags = diags.Append(writeOnlyDiags) @@ -743,7 +743,7 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs. return nil, deferred, diags } - importedState := imported[0].AsInstanceObject() + importedState := states.NewResourceInstanceObjectFromIR(imported[0]) if deferred == nil && importedState.Value.IsNull() { // It's actually okay for a deferred import to have returned a null. diags = diags.Append(tfdiags.Sourceless( @@ -807,7 +807,7 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs. // First we generate the contents of the resource block for use within // the planning node. Then we wrap it in an enclosing resource block to // pass into the plan for rendering. - generatedHCLAttributes, generatedDiags := n.generateHCLStringAttributes(n.Addr, instanceRefreshState, schema) + generatedHCLAttributes, generatedDiags := n.generateHCLStringAttributes(n.Addr, instanceRefreshState, schema.Body) diags = diags.Append(generatedDiags) n.generatedConfigHCL = genconfig.WrapResourceContents(n.Addr, generatedHCLAttributes) diff --git a/internal/terraform/node_resource_plan_partialexp.go b/internal/terraform/node_resource_plan_partialexp.go index 8a8590882687..d2675b0449fe 100644 --- a/internal/terraform/node_resource_plan_partialexp.go +++ b/internal/terraform/node_resource_plan_partialexp.go @@ -165,8 +165,8 @@ func (n *nodePlannablePartialExpandedResource) managedResourceExecute(ctx EvalCo return &change, diags } - schema, _ := providerSchema.SchemaForResourceAddr(n.addr.Resource()) - if schema == nil { + schema := providerSchema.SchemaForResourceAddr(n.addr.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 %q", n.addr.Resource().Type)) return &change, diags @@ -194,7 +194,7 @@ func (n *nodePlannablePartialExpandedResource) managedResourceExecute(ctx EvalCo keyData := n.keyData() - configVal, _, configDiags := ctx.EvaluateBlock(n.config.Config, schema, nil, keyData) + configVal, _, configDiags := ctx.EvaluateBlock(n.config.Config, schema.Body, nil, keyData) diags = diags.Append(configDiags) if configDiags.HasErrors() { return &change, diags @@ -214,8 +214,8 @@ func (n *nodePlannablePartialExpandedResource) managedResourceExecute(ctx EvalCo } unmarkedConfigVal, unmarkedPaths := configVal.UnmarkDeepWithPaths() - priorVal := cty.NullVal(schema.ImpliedType()) // we don't have any specific prior value to use - proposedNewVal := objchange.ProposedNew(schema, priorVal, unmarkedConfigVal) + priorVal := cty.NullVal(schema.Body.ImpliedType()) // we don't have any specific prior value to use + proposedNewVal := objchange.ProposedNew(schema.Body, priorVal, unmarkedConfigVal) // The provider now gets to plan an imaginary substitute that represents // all of the possible resource instances together. Correctly-implemented @@ -247,7 +247,7 @@ func (n *nodePlannablePartialExpandedResource) managedResourceExecute(ctx EvalCo panic(fmt.Sprintf("PlanResourceChange of %s produced nil value", n.addr.String())) } - for _, err := range plannedNewVal.Type().TestConformance(schema.ImpliedType()) { + for _, err := range plannedNewVal.Type().TestConformance(schema.Body.ImpliedType()) { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Provider produced invalid plan", @@ -261,7 +261,7 @@ func (n *nodePlannablePartialExpandedResource) managedResourceExecute(ctx EvalCo return &change, diags } - if errs := objchange.AssertPlanValid(schema, priorVal, unmarkedConfigVal, plannedNewVal); len(errs) > 0 { + if errs := objchange.AssertPlanValid(schema.Body, priorVal, unmarkedConfigVal, plannedNewVal); len(errs) > 0 { if resp.LegacyTypeSystem { // The shimming of the old type system in the legacy SDK is not precise // enough to pass this consistency check, so we'll give it a pass here, @@ -295,7 +295,7 @@ func (n *nodePlannablePartialExpandedResource) managedResourceExecute(ctx EvalCo // We need to combine the dynamic marks with the static marks implied by // the provider's schema. plannedNewVal = plannedNewVal.MarkWithPaths(unmarkedPaths) - if sensitivePaths := schema.SensitivePaths(plannedNewVal, nil); len(sensitivePaths) != 0 { + if sensitivePaths := schema.Body.SensitivePaths(plannedNewVal, nil); len(sensitivePaths) != 0 { plannedNewVal = marks.MarkPaths(plannedNewVal, marks.Sensitive, sensitivePaths) } @@ -339,8 +339,8 @@ func (n *nodePlannablePartialExpandedResource) dataResourceExecute(ctx EvalConte // This is the point where we switch to mirroring logic from // NodeAbstractResourceInstance's planDataSource. If you were curious. - schema, _ := providerSchema.SchemaForResourceAddr(n.addr.Resource()) - if schema == nil { + schema := providerSchema.SchemaForResourceAddr(n.addr.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 %q", n.addr.Resource().Type)) return &change, diags @@ -348,7 +348,7 @@ func (n *nodePlannablePartialExpandedResource) dataResourceExecute(ctx EvalConte keyData := n.keyData() - configVal, _, configDiags := ctx.EvaluateBlock(n.config.Config, schema, nil, keyData) + configVal, _, configDiags := ctx.EvaluateBlock(n.config.Config, schema.Body, nil, keyData) diags = diags.Append(configDiags) if configDiags.HasErrors() { return &change, diags @@ -369,9 +369,9 @@ func (n *nodePlannablePartialExpandedResource) dataResourceExecute(ctx EvalConte // logic for a data source with unknown config, which is sort of what we // are, after all. unmarkedConfigVal, unmarkedPaths := configVal.UnmarkDeepWithPaths() - proposedNewVal := objchange.PlannedDataResourceObject(schema, unmarkedConfigVal) + proposedNewVal := objchange.PlannedDataResourceObject(schema.Body, unmarkedConfigVal) proposedNewVal = proposedNewVal.MarkWithPaths(unmarkedPaths) - if sensitivePaths := schema.SensitivePaths(proposedNewVal, nil); len(sensitivePaths) != 0 { + if sensitivePaths := schema.Body.SensitivePaths(proposedNewVal, nil); len(sensitivePaths) != 0 { proposedNewVal = marks.MarkPaths(proposedNewVal, marks.Sensitive, sensitivePaths) } // yay we made it diff --git a/internal/terraform/node_resource_validate.go b/internal/terraform/node_resource_validate.go index 728ef89250f9..bb9f8bc401f9 100644 --- a/internal/terraform/node_resource_validate.go +++ b/internal/terraform/node_resource_validate.go @@ -323,10 +323,10 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag // in the provider abstraction. switch n.Config.Mode { case addrs.ManagedResourceMode: - schema, _ := providerSchema.SchemaForResourceType(n.Config.Mode, n.Config.Type) - if schema == nil { + schema := providerSchema.SchemaForResourceType(n.Config.Mode, n.Config.Type) + if schema.Body == nil { var suggestion string - if dSchema, _ := providerSchema.SchemaForResourceType(addrs.DataResourceMode, n.Config.Type); dSchema != nil { + if dSchema := providerSchema.SchemaForResourceType(addrs.DataResourceMode, n.Config.Type); dSchema.Body != nil { suggestion = fmt.Sprintf("\n\nDid you intend to use the data source %q? If so, declare this using a \"data\" block instead of a \"resource\" block.", n.Config.Type) } else if len(providerSchema.ResourceTypes) > 0 { suggestions := make([]string, 0, len(providerSchema.ResourceTypes)) @@ -347,19 +347,19 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag return diags } - configVal, _, valDiags := ctx.EvaluateBlock(n.Config.Config, schema, nil, keyData) + configVal, _, valDiags := ctx.EvaluateBlock(n.Config.Config, schema.Body, nil, keyData) diags = diags.Append(valDiags) if valDiags.HasErrors() { return diags } diags = diags.Append( - validateResourceForbiddenEphemeralValues(ctx, configVal, schema).InConfigBody(n.Config.Config, n.Addr.String()), + validateResourceForbiddenEphemeralValues(ctx, configVal, schema.Body).InConfigBody(n.Config.Config, n.Addr.String()), ) if n.Config.Managed != nil { // can be nil only in tests with poorly-configured mocks for _, traversal := range n.Config.Managed.IgnoreChanges { // validate the ignore_changes traversals apply. - moreDiags := schema.StaticValidateTraversal(traversal) + moreDiags := schema.Body.StaticValidateTraversal(traversal) diags = diags.Append(moreDiags) // ignore_changes cannot be used for Computed attributes, @@ -370,7 +370,7 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag if !diags.HasErrors() { path, _ := traversalToPath(traversal) - attrSchema := schema.AttributeByPath(path) + attrSchema := schema.Body.AttributeByPath(path) if attrSchema != nil && !attrSchema.Optional && attrSchema.Computed { // ignore_changes uses absolute traversal syntax in config despite @@ -402,10 +402,10 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config, n.Addr.String())) case addrs.DataResourceMode: - schema, _ := providerSchema.SchemaForResourceType(n.Config.Mode, n.Config.Type) - if schema == nil { + schema := providerSchema.SchemaForResourceType(n.Config.Mode, n.Config.Type) + if schema.Body == nil { var suggestion string - if dSchema, _ := providerSchema.SchemaForResourceType(addrs.ManagedResourceMode, n.Config.Type); dSchema != nil { + if dSchema := providerSchema.SchemaForResourceType(addrs.ManagedResourceMode, n.Config.Type); dSchema.Body != nil { suggestion = fmt.Sprintf("\n\nDid you intend to use the managed resource type %q? If so, declare this using a \"resource\" block instead of a \"data\" block.", n.Config.Type) } else if len(providerSchema.DataSources) > 0 { suggestions := make([]string, 0, len(providerSchema.DataSources)) @@ -426,13 +426,13 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag return diags } - configVal, _, valDiags := ctx.EvaluateBlock(n.Config.Config, schema, nil, keyData) + configVal, _, valDiags := ctx.EvaluateBlock(n.Config.Config, schema.Body, nil, keyData) diags = diags.Append(valDiags) if valDiags.HasErrors() { return diags } diags = diags.Append( - validateResourceForbiddenEphemeralValues(ctx, configVal, schema).InConfigBody(n.Config.Config, n.Addr.String()), + validateResourceForbiddenEphemeralValues(ctx, configVal, schema.Body).InConfigBody(n.Config.Config, n.Addr.String()), ) // Use unmarked value for validate request @@ -445,8 +445,8 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag resp := provider.ValidateDataResourceConfig(req) diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config, n.Addr.String())) case addrs.EphemeralResourceMode: - schema, _ := providerSchema.SchemaForResourceType(n.Config.Mode, n.Config.Type) - if schema == nil { + schema := providerSchema.SchemaForResourceType(n.Config.Mode, n.Config.Type) + if schema.Body == nil { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid ephemeral resource", @@ -456,7 +456,7 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag return diags } - configVal, _, valDiags := ctx.EvaluateBlock(n.Config.Config, schema, nil, keyData) + configVal, _, valDiags := ctx.EvaluateBlock(n.Config.Config, schema.Body, nil, keyData) diags = diags.Append(valDiags) if valDiags.HasErrors() { return diags diff --git a/internal/terraform/resource_provider_mock_test.go b/internal/terraform/resource_provider_mock_test.go index f0b8006bd8a4..45da48cbf1b5 100644 --- a/internal/terraform/resource_provider_mock_test.go +++ b/internal/terraform/resource_provider_mock_test.go @@ -124,6 +124,8 @@ type providerSchema struct { ResourceTypes map[string]*configschema.Block ResourceTypeSchemaVersions map[string]uint64 DataSources map[string]*configschema.Block + IdentityTypes map[string]*configschema.Object + IdentityTypeSchemaVersions map[string]uint64 } // getProviderSchemaResponseFromProviderSchema is a test helper to convert a @@ -137,10 +139,18 @@ func getProviderSchemaResponseFromProviderSchema(providerSchema *providerSchema) } for name, schema := range providerSchema.ResourceTypes { - resp.ResourceTypes[name] = providers.Schema{ + ps := providers.Schema{ Body: schema, Version: int64(providerSchema.ResourceTypeSchemaVersions[name]), } + + id, ok := providerSchema.IdentityTypes[name] + if ok { + ps.Identity = id + ps.IdentityVersion = int64(providerSchema.IdentityTypeSchemaVersions[name]) + } + + resp.ResourceTypes[name] = ps } for name, schema := range providerSchema.DataSources { diff --git a/internal/terraform/transform_attach_schema.go b/internal/terraform/transform_attach_schema.go index 74f9ffb779be..8d70e7285864 100644 --- a/internal/terraform/transform_attach_schema.go +++ b/internal/terraform/transform_attach_schema.go @@ -65,16 +65,16 @@ func (t *AttachSchemaTransformer) Transform(g *Graph) error { typeName := addr.Resource.Type providerFqn := tv.Provider() - schema, version, err := t.Plugins.ResourceTypeSchema(providerFqn, mode, typeName) + schema, err := t.Plugins.ResourceTypeSchema(providerFqn, mode, typeName) if err != nil { return fmt.Errorf("failed to read schema for %s in %s: %s", addr, providerFqn, err) } - if schema == nil { + if schema.Body == nil { log.Printf("[ERROR] AttachSchemaTransformer: No resource schema available for %s", addr) continue } log.Printf("[TRACE] AttachSchemaTransformer: attaching resource schema to %s", dag.VertexName(v)) - tv.AttachResourceSchema(schema, version) + tv.AttachResourceSchema(schema.Body, uint64(schema.Version)) } if tv, ok := v.(GraphNodeAttachProviderConfigSchema); ok { diff --git a/internal/terraform/upgrade_resource_state.go b/internal/terraform/upgrade_resource_state.go index f8e84a029b9d..7e6b698718bb 100644 --- a/internal/terraform/upgrade_resource_state.go +++ b/internal/terraform/upgrade_resource_state.go @@ -10,7 +10,6 @@ import ( "log" "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/lang/ephemeral" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" @@ -25,7 +24,7 @@ import ( // // If any errors occur during upgrade, error diagnostics are returned. In that // case it is not safe to proceed with using the original state object. -func upgradeResourceState(addr addrs.AbsResourceInstance, provider providers.Interface, src *states.ResourceInstanceObjectSrc, currentSchema *configschema.Block, currentVersion uint64) (*states.ResourceInstanceObjectSrc, tfdiags.Diagnostics) { +func upgradeResourceState(addr addrs.AbsResourceInstance, provider providers.Interface, src *states.ResourceInstanceObjectSrc, currentSchema providers.Schema) (*states.ResourceInstanceObjectSrc, tfdiags.Diagnostics) { if addr.Resource.Resource.Mode != addrs.ManagedResourceMode { // We only do state upgrading for managed resources. // This was a part of the normal workflow in older versions and @@ -41,16 +40,16 @@ func upgradeResourceState(addr addrs.AbsResourceInstance, provider providers.Int // Legacy flatmap state is already taken care of during conversion. // If the schema version is be changed, then allow the provider to handle // removed attributes. - if len(src.AttrsJSON) > 0 && src.SchemaVersion == currentVersion { - src.AttrsJSON = stripRemovedStateAttributes(src.AttrsJSON, currentSchema.ImpliedType()) + if len(src.AttrsJSON) > 0 && src.SchemaVersion == uint64(currentSchema.Version) { + src.AttrsJSON = stripRemovedStateAttributes(src.AttrsJSON, currentSchema.Body.ImpliedType()) } stateIsFlatmap := len(src.AttrsJSON) == 0 // TODO: This should eventually use a proper FQN. providerType := addr.Resource.Resource.ImpliedProvider() - if src.SchemaVersion > currentVersion { - log.Printf("[TRACE] upgradeResourceState: can't downgrade state for %s from version %d to %d", addr, src.SchemaVersion, currentVersion) + if src.SchemaVersion > uint64(currentSchema.Version) { + log.Printf("[TRACE] upgradeResourceState: can't downgrade state for %s from version %d to %d", addr, src.SchemaVersion, currentSchema.Version) var diags tfdiags.Diagnostics diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, @@ -69,10 +68,10 @@ func upgradeResourceState(addr addrs.AbsResourceInstance, provider providers.Int // v0.12, this also includes translating from legacy flatmap to new-style // representation, since only the provider has enough information to // understand a flatmap built against an older schema. - if src.SchemaVersion != currentVersion { - log.Printf("[TRACE] upgradeResourceState: upgrading state for %s from version %d to %d using provider %q", addr, src.SchemaVersion, currentVersion, providerType) + if src.SchemaVersion != uint64(currentSchema.Version) { + log.Printf("[TRACE] upgradeResourceState: upgrading state for %s from version %d to %d using provider %q", addr, src.SchemaVersion, currentSchema.Version, providerType) } else { - log.Printf("[TRACE] upgradeResourceState: schema version of %s is still %d; calling provider %q for any other minor fixups", addr, currentVersion, providerType) + log.Printf("[TRACE] upgradeResourceState: schema version of %s is still %d; calling provider %q for any other minor fixups", addr, currentSchema.Version, providerType) } req := providers.UpgradeResourceStateRequest{ @@ -111,7 +110,7 @@ func upgradeResourceState(addr addrs.AbsResourceInstance, provider providers.Int // marshaling/unmarshaling of the new value, but we'll check it here // anyway for robustness, e.g. for in-process providers. newValue := resp.UpgradedState - if errs := newValue.Type().TestConformance(currentSchema.ImpliedType()); len(errs) > 0 { + if errs := newValue.Type().TestConformance(currentSchema.Body.ImpliedType()); len(errs) > 0 { for _, err := range errs { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, @@ -132,7 +131,7 @@ func upgradeResourceState(addr addrs.AbsResourceInstance, provider providers.Int ) }, newValue, - currentSchema, + currentSchema.Body, ) diags = diags.Append(writeOnlyDiags) @@ -140,7 +139,7 @@ func upgradeResourceState(addr addrs.AbsResourceInstance, provider providers.Int return nil, diags } - new, err := src.CompleteUpgrade(newValue, currentSchema.ImpliedType(), uint64(currentVersion)) + new, err := src.CompleteUpgrade(newValue, currentSchema.Body.ImpliedType(), uint64(currentSchema.Version)) if err != nil { // We already checked for type conformance above, so getting into this // codepath should be rare and is probably a bug somewhere under CompleteUpgrade. @@ -153,6 +152,83 @@ func upgradeResourceState(addr addrs.AbsResourceInstance, provider providers.Int return new, diags } +func upgradeResourceIdentity(addr addrs.AbsResourceInstance, provider providers.Interface, src *states.ResourceInstanceObjectSrc, currentSchema providers.Schema) (*states.ResourceInstanceObjectSrc, tfdiags.Diagnostics) { + // TODO: This should eventually use a proper FQN. + providerType := addr.Resource.Resource.ImpliedProvider() + if src.IdentitySchemaVersion > uint64(currentSchema.IdentityVersion) { + log.Printf("[TRACE] upgradeResourceIdentity: can't downgrade identity for %s from version %d to %d", addr, src.IdentitySchemaVersion, currentSchema.IdentityVersion) + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Resource instance managed by newer provider version", + // This is not a very good error message, but we don't retain enough + // information in state to give good feedback on what provider + // version might be required here. :( + fmt.Sprintf("The current state of %s was created by a newer provider version than is currently selected. Upgrade the %s provider to work with this state.", addr, providerType), + )) + return nil, diags + } + + // We don't need to do anything if the identity schema version is already up-to-date. + if src.IdentitySchemaVersion == uint64(currentSchema.IdentityVersion) { + return src, nil + } + + req := providers.UpgradeResourceIdentityRequest{ + TypeName: addr.Resource.Resource.Type, + + // TODO: The internal schema version representations are all using + // uint64 instead of int64, but unsigned integers aren't friendly + // to all protobuf target languages so in practice we use int64 + // on the wire. In future we will change all of our internal + // representations to int64 too. + Version: int64(src.SchemaVersion), + RawIdentityJSON: src.IdentityJSON, + } + + resp := provider.UpgradeResourceIdentity(req) + diags := resp.Diagnostics + if diags.HasErrors() { + return nil, diags + } + + if !resp.UpgradedIdentity.IsWhollyKnown() { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid resource identity upgrade", + fmt.Sprintf("The %s provider upgraded the identity for %s from a previous version, but produced an invalid result: The returned state contains unknown values.", providerType, addr), + )) + return nil, diags + } + + newIdentity := resp.UpgradedIdentity + newType := newIdentity.Type() + currentType := currentSchema.Identity.ImpliedType() + if errs := newType.TestConformance(currentType); len(errs) > 0 { + for _, err := range errs { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid resource identity upgrade", + fmt.Sprintf("The %s provider upgraded the identity for %s from a previous version, but produced an invalid result: %s.", providerType, addr, tfdiags.FormatError(err)), + )) + } + return nil, diags + } + + new, err := src.CompleteIdentityUpgrade(newIdentity, currentSchema) + if err != nil { + // We already checked for type conformance above, so getting into this + // codepath should be rare and is probably a bug somewhere under CompleteUpgrade. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to encode result of resource identity upgrade", + fmt.Sprintf("Failed to encode state for %s after resource identity schema upgrade: %s.", addr, tfdiags.FormatError(err)), + )) + } + + return new, diags +} + // stripRemovedStateAttributes deletes any attributes no longer present in the // schema, so that the json can be correctly decoded. func stripRemovedStateAttributes(state []byte, ty cty.Type) []byte { diff --git a/internal/terraform/validate_selfref.go b/internal/terraform/validate_selfref.go index 521ecae16b96..e8d19f7db7d3 100644 --- a/internal/terraform/validate_selfref.go +++ b/internal/terraform/validate_selfref.go @@ -9,7 +9,6 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/lang/langrefs" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/tfdiags" @@ -28,20 +27,20 @@ func validateSelfRef(addr addrs.Referenceable, config hcl.Body, providerSchema p addrStrs = append(addrStrs, tAddr.ContainingResource().String()) } - var schema *configschema.Block + var schema providers.Schema switch tAddr := addr.(type) { case addrs.Resource: - schema, _ = providerSchema.SchemaForResourceAddr(tAddr) + schema = providerSchema.SchemaForResourceAddr(tAddr) case addrs.ResourceInstance: - schema, _ = providerSchema.SchemaForResourceAddr(tAddr.ContainingResource()) + schema = providerSchema.SchemaForResourceAddr(tAddr.ContainingResource()) } - if schema == nil { + if schema.Body == nil { diags = diags.Append(fmt.Errorf("no schema available for %s to validate for self-references; this is a bug in Terraform and should be reported", addr)) return diags } - refs, _ := langrefs.ReferencesInBlock(addrs.ParseRef, config, schema) + refs, _ := langrefs.ReferencesInBlock(addrs.ParseRef, config, schema.Body) for _, ref := range refs { for _, addrStr := range addrStrs { if ref.Subject.String() == addrStr {