1539 lines
49 KiB
Go
1539 lines
49 KiB
Go
package schema
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"sync"
|
|
|
|
"github.com/hashicorp/go-cty/cty"
|
|
ctyconvert "github.com/hashicorp/go-cty/cty/convert"
|
|
"github.com/hashicorp/go-cty/cty/msgpack"
|
|
|
|
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
|
|
"github.com/hashicorp/terraform-plugin-go/tftypes"
|
|
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/configs/configschema"
|
|
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/configs/hcl2shim"
|
|
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/logging"
|
|
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/plans/objchange"
|
|
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/plugin/convert"
|
|
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
|
|
)
|
|
|
|
const (
|
|
newExtraKey = "_new_extra_shim"
|
|
)
|
|
|
|
func NewGRPCProviderServer(p *Provider) *GRPCProviderServer {
|
|
return &GRPCProviderServer{
|
|
provider: p,
|
|
stopCh: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// GRPCProviderServer handles the server, or plugin side of the rpc connection.
|
|
type GRPCProviderServer struct {
|
|
provider *Provider
|
|
stopCh chan struct{}
|
|
stopMu sync.Mutex
|
|
}
|
|
|
|
// mergeStop is called in a goroutine and waits for the global stop signal
|
|
// and propagates cancellation to the passed in ctx/cancel func. The ctx is
|
|
// also passed to this function and waited upon so no goroutine leak is caused.
|
|
func mergeStop(ctx context.Context, cancel context.CancelFunc, stopCh chan struct{}) {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-stopCh:
|
|
cancel()
|
|
}
|
|
}
|
|
|
|
// StopContext derives a new context from the passed in grpc context.
|
|
// It creates a goroutine to wait for the server stop and propagates
|
|
// cancellation to the derived grpc context.
|
|
func (s *GRPCProviderServer) StopContext(ctx context.Context) context.Context {
|
|
ctx = logging.InitContext(ctx)
|
|
s.stopMu.Lock()
|
|
defer s.stopMu.Unlock()
|
|
|
|
stoppable, cancel := context.WithCancel(ctx)
|
|
go mergeStop(stoppable, cancel, s.stopCh)
|
|
return stoppable
|
|
}
|
|
|
|
func (s *GRPCProviderServer) GetProviderSchema(ctx context.Context, req *tfprotov5.GetProviderSchemaRequest) (*tfprotov5.GetProviderSchemaResponse, error) {
|
|
ctx = logging.InitContext(ctx)
|
|
|
|
logging.HelperSchemaTrace(ctx, "Getting provider schema")
|
|
|
|
resp := &tfprotov5.GetProviderSchemaResponse{
|
|
ResourceSchemas: make(map[string]*tfprotov5.Schema),
|
|
DataSourceSchemas: make(map[string]*tfprotov5.Schema),
|
|
}
|
|
|
|
resp.Provider = &tfprotov5.Schema{
|
|
Block: convert.ConfigSchemaToProto(ctx, s.getProviderSchemaBlock()),
|
|
}
|
|
|
|
resp.ProviderMeta = &tfprotov5.Schema{
|
|
Block: convert.ConfigSchemaToProto(ctx, s.getProviderMetaSchemaBlock()),
|
|
}
|
|
|
|
for typ, res := range s.provider.ResourcesMap {
|
|
logging.HelperSchemaTrace(ctx, "Found resource type", map[string]interface{}{logging.KeyResourceType: typ})
|
|
|
|
resp.ResourceSchemas[typ] = &tfprotov5.Schema{
|
|
Version: int64(res.SchemaVersion),
|
|
Block: convert.ConfigSchemaToProto(ctx, res.CoreConfigSchema()),
|
|
}
|
|
}
|
|
|
|
for typ, dat := range s.provider.DataSourcesMap {
|
|
logging.HelperSchemaTrace(ctx, "Found data source type", map[string]interface{}{logging.KeyDataSourceType: typ})
|
|
|
|
resp.DataSourceSchemas[typ] = &tfprotov5.Schema{
|
|
Version: int64(dat.SchemaVersion),
|
|
Block: convert.ConfigSchemaToProto(ctx, dat.CoreConfigSchema()),
|
|
}
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (s *GRPCProviderServer) getProviderSchemaBlock() *configschema.Block {
|
|
return InternalMap(s.provider.Schema).CoreConfigSchema()
|
|
}
|
|
|
|
func (s *GRPCProviderServer) getProviderMetaSchemaBlock() *configschema.Block {
|
|
return InternalMap(s.provider.ProviderMetaSchema).CoreConfigSchema()
|
|
}
|
|
|
|
func (s *GRPCProviderServer) getResourceSchemaBlock(name string) *configschema.Block {
|
|
res := s.provider.ResourcesMap[name]
|
|
return res.CoreConfigSchema()
|
|
}
|
|
|
|
func (s *GRPCProviderServer) getDatasourceSchemaBlock(name string) *configschema.Block {
|
|
dat := s.provider.DataSourcesMap[name]
|
|
return dat.CoreConfigSchema()
|
|
}
|
|
|
|
func (s *GRPCProviderServer) PrepareProviderConfig(ctx context.Context, req *tfprotov5.PrepareProviderConfigRequest) (*tfprotov5.PrepareProviderConfigResponse, error) {
|
|
ctx = logging.InitContext(ctx)
|
|
resp := &tfprotov5.PrepareProviderConfigResponse{}
|
|
|
|
logging.HelperSchemaTrace(ctx, "Preparing provider configuration")
|
|
|
|
schemaBlock := s.getProviderSchemaBlock()
|
|
|
|
configVal, err := msgpack.Unmarshal(req.Config.MsgPack, schemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
// lookup any required, top-level attributes that are Null, and see if we
|
|
// have a Default value available.
|
|
configVal, err = cty.Transform(configVal, func(path cty.Path, val cty.Value) (cty.Value, error) {
|
|
// we're only looking for top-level attributes
|
|
if len(path) != 1 {
|
|
return val, nil
|
|
}
|
|
|
|
// nothing to do if we already have a value
|
|
if !val.IsNull() {
|
|
return val, nil
|
|
}
|
|
|
|
// get the Schema definition for this attribute
|
|
getAttr, ok := path[0].(cty.GetAttrStep)
|
|
// these should all exist, but just ignore anything strange
|
|
if !ok {
|
|
return val, nil
|
|
}
|
|
|
|
attrSchema := s.provider.Schema[getAttr.Name]
|
|
// continue to ignore anything that doesn't match
|
|
if attrSchema == nil {
|
|
return val, nil
|
|
}
|
|
|
|
// this is deprecated, so don't set it
|
|
if attrSchema.Deprecated != "" {
|
|
return val, nil
|
|
}
|
|
|
|
// find a default value if it exists
|
|
def, err := attrSchema.DefaultValue()
|
|
if err != nil {
|
|
return val, fmt.Errorf("error getting default for %q: %w", getAttr.Name, err)
|
|
}
|
|
|
|
// no default
|
|
if def == nil {
|
|
return val, nil
|
|
}
|
|
|
|
// create a cty.Value and make sure it's the correct type
|
|
tmpVal := hcl2shim.HCL2ValueFromConfigValue(def)
|
|
|
|
// helper/schema used to allow setting "" to a bool
|
|
if val.Type() == cty.Bool && tmpVal.RawEquals(cty.StringVal("")) {
|
|
// return a warning about the conversion
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, "provider set empty string as default value for bool "+getAttr.Name)
|
|
tmpVal = cty.False
|
|
}
|
|
|
|
val, err = ctyconvert.Convert(tmpVal, val.Type())
|
|
if err != nil {
|
|
return val, fmt.Errorf("error setting default for %q: %w", getAttr.Name, err)
|
|
}
|
|
|
|
return val, nil
|
|
})
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
configVal, err = schemaBlock.CoerceValue(configVal)
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
// Ensure there are no nulls that will cause helper/schema to panic.
|
|
if err := validateConfigNulls(ctx, configVal, nil); err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
config := terraform.NewResourceConfigShimmed(configVal, schemaBlock)
|
|
|
|
logging.HelperSchemaTrace(ctx, "Calling downstream")
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, s.provider.Validate(config))
|
|
logging.HelperSchemaTrace(ctx, "Called downstream")
|
|
|
|
preparedConfigMP, err := msgpack.Marshal(configVal, schemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
resp.PreparedConfig = &tfprotov5.DynamicValue{MsgPack: preparedConfigMP}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (s *GRPCProviderServer) ValidateResourceTypeConfig(ctx context.Context, req *tfprotov5.ValidateResourceTypeConfigRequest) (*tfprotov5.ValidateResourceTypeConfigResponse, error) {
|
|
ctx = logging.InitContext(ctx)
|
|
resp := &tfprotov5.ValidateResourceTypeConfigResponse{}
|
|
|
|
schemaBlock := s.getResourceSchemaBlock(req.TypeName)
|
|
|
|
configVal, err := msgpack.Unmarshal(req.Config.MsgPack, schemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
config := terraform.NewResourceConfigShimmed(configVal, schemaBlock)
|
|
|
|
logging.HelperSchemaTrace(ctx, "Calling downstream")
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, s.provider.ValidateResource(req.TypeName, config))
|
|
logging.HelperSchemaTrace(ctx, "Called downstream")
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (s *GRPCProviderServer) ValidateDataSourceConfig(ctx context.Context, req *tfprotov5.ValidateDataSourceConfigRequest) (*tfprotov5.ValidateDataSourceConfigResponse, error) {
|
|
ctx = logging.InitContext(ctx)
|
|
resp := &tfprotov5.ValidateDataSourceConfigResponse{}
|
|
|
|
schemaBlock := s.getDatasourceSchemaBlock(req.TypeName)
|
|
|
|
configVal, err := msgpack.Unmarshal(req.Config.MsgPack, schemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
// Ensure there are no nulls that will cause helper/schema to panic.
|
|
if err := validateConfigNulls(ctx, configVal, nil); err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
config := terraform.NewResourceConfigShimmed(configVal, schemaBlock)
|
|
|
|
logging.HelperSchemaTrace(ctx, "Calling downstream")
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, s.provider.ValidateDataSource(req.TypeName, config))
|
|
logging.HelperSchemaTrace(ctx, "Called downstream")
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (s *GRPCProviderServer) UpgradeResourceState(ctx context.Context, req *tfprotov5.UpgradeResourceStateRequest) (*tfprotov5.UpgradeResourceStateResponse, error) {
|
|
ctx = logging.InitContext(ctx)
|
|
resp := &tfprotov5.UpgradeResourceStateResponse{}
|
|
|
|
res, ok := s.provider.ResourcesMap[req.TypeName]
|
|
if !ok {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, fmt.Errorf("unknown resource type: %s", req.TypeName))
|
|
return resp, nil
|
|
}
|
|
schemaBlock := s.getResourceSchemaBlock(req.TypeName)
|
|
|
|
version := int(req.Version)
|
|
|
|
jsonMap := map[string]interface{}{}
|
|
var err error
|
|
|
|
switch {
|
|
// We first need to upgrade a flatmap state if it exists.
|
|
// There should never be both a JSON and Flatmap state in the request.
|
|
case len(req.RawState.Flatmap) > 0:
|
|
logging.HelperSchemaTrace(ctx, "Upgrading flatmap state")
|
|
|
|
jsonMap, version, err = s.upgradeFlatmapState(ctx, version, req.RawState.Flatmap, res)
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
// if there's a JSON state, we need to decode it.
|
|
case len(req.RawState.JSON) > 0:
|
|
if res.UseJSONNumber {
|
|
err = unmarshalJSON(req.RawState.JSON, &jsonMap)
|
|
} else {
|
|
err = json.Unmarshal(req.RawState.JSON, &jsonMap)
|
|
}
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
default:
|
|
logging.HelperSchemaDebug(ctx, "no state provided to upgrade")
|
|
return resp, nil
|
|
}
|
|
|
|
// complete the upgrade of the JSON states
|
|
logging.HelperSchemaTrace(ctx, "Upgrading JSON state")
|
|
|
|
jsonMap, err = s.upgradeJSONState(ctx, version, jsonMap, res)
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
// The provider isn't required to clean out removed fields
|
|
s.removeAttributes(ctx, jsonMap, schemaBlock.ImpliedType())
|
|
|
|
// now we need to turn the state into the default json representation, so
|
|
// that it can be re-decoded using the actual schema.
|
|
val, err := JSONMapToStateValue(jsonMap, schemaBlock)
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
// Now we need to make sure blocks are represented correctly, which means
|
|
// that missing blocks are empty collections, rather than null.
|
|
// First we need to CoerceValue to ensure that all object types match.
|
|
val, err = schemaBlock.CoerceValue(val)
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
// Normalize the value and fill in any missing blocks.
|
|
val = objchange.NormalizeObjectFromLegacySDK(val, schemaBlock)
|
|
|
|
// encode the final state to the expected msgpack format
|
|
newStateMP, err := msgpack.Marshal(val, schemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
resp.UpgradedState = &tfprotov5.DynamicValue{MsgPack: newStateMP}
|
|
return resp, nil
|
|
}
|
|
|
|
// upgradeFlatmapState takes a legacy flatmap state, upgrades it using Migrate
|
|
// state if necessary, and converts it to the new JSON state format decoded as a
|
|
// map[string]interface{}.
|
|
// upgradeFlatmapState returns the json map along with the corresponding schema
|
|
// version.
|
|
func (s *GRPCProviderServer) upgradeFlatmapState(_ context.Context, version int, m map[string]string, res *Resource) (map[string]interface{}, int, error) {
|
|
// this will be the version we've upgraded so, defaulting to the given
|
|
// version in case no migration was called.
|
|
upgradedVersion := version
|
|
|
|
// first determine if we need to call the legacy MigrateState func
|
|
requiresMigrate := version < res.SchemaVersion
|
|
|
|
schemaType := res.CoreConfigSchema().ImpliedType()
|
|
|
|
// if there are any StateUpgraders, then we need to only compare
|
|
// against the first version there
|
|
if len(res.StateUpgraders) > 0 {
|
|
requiresMigrate = version < res.StateUpgraders[0].Version
|
|
}
|
|
|
|
if requiresMigrate && res.MigrateState == nil {
|
|
// Providers were previously allowed to bump the version
|
|
// without declaring MigrateState.
|
|
// If there are further upgraders, then we've only updated that far.
|
|
if len(res.StateUpgraders) > 0 {
|
|
schemaType = res.StateUpgraders[0].Type
|
|
upgradedVersion = res.StateUpgraders[0].Version
|
|
}
|
|
} else if requiresMigrate {
|
|
is := &terraform.InstanceState{
|
|
ID: m["id"],
|
|
Attributes: m,
|
|
Meta: map[string]interface{}{
|
|
"schema_version": strconv.Itoa(version),
|
|
},
|
|
}
|
|
is, err := res.MigrateState(version, is, s.provider.Meta())
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// re-assign the map in case there was a copy made, making sure to keep
|
|
// the ID
|
|
m := is.Attributes
|
|
m["id"] = is.ID
|
|
|
|
// if there are further upgraders, then we've only updated that far
|
|
if len(res.StateUpgraders) > 0 {
|
|
schemaType = res.StateUpgraders[0].Type
|
|
upgradedVersion = res.StateUpgraders[0].Version
|
|
}
|
|
} else {
|
|
// the schema version may be newer than the MigrateState functions
|
|
// handled and older than the current, but still stored in the flatmap
|
|
// form. If that's the case, we need to find the correct schema type to
|
|
// convert the state.
|
|
for _, upgrader := range res.StateUpgraders {
|
|
if upgrader.Version == version {
|
|
schemaType = upgrader.Type
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// now we know the state is up to the latest version that handled the
|
|
// flatmap format state. Now we can upgrade the format and continue from
|
|
// there.
|
|
newConfigVal, err := hcl2shim.HCL2ValueFromFlatmap(m, schemaType)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
jsonMap, err := stateValueToJSONMap(newConfigVal, schemaType, res.UseJSONNumber)
|
|
return jsonMap, upgradedVersion, err
|
|
}
|
|
|
|
func (s *GRPCProviderServer) upgradeJSONState(ctx context.Context, version int, m map[string]interface{}, res *Resource) (map[string]interface{}, error) {
|
|
var err error
|
|
|
|
for _, upgrader := range res.StateUpgraders {
|
|
if version != upgrader.Version {
|
|
continue
|
|
}
|
|
|
|
m, err = upgrader.Upgrade(ctx, m, s.provider.Meta())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
version++
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
// Remove any attributes no longer present in the schema, so that the json can
|
|
// be correctly decoded.
|
|
func (s *GRPCProviderServer) removeAttributes(ctx context.Context, v interface{}, ty cty.Type) {
|
|
// we're only concerned with finding maps that corespond to object
|
|
// attributes
|
|
switch v := v.(type) {
|
|
case []interface{}:
|
|
// If these aren't blocks the next call will be a noop
|
|
if ty.IsListType() || ty.IsSetType() {
|
|
eTy := ty.ElementType()
|
|
for _, eV := range v {
|
|
s.removeAttributes(ctx, eV, eTy)
|
|
}
|
|
}
|
|
return
|
|
case map[string]interface{}:
|
|
// map blocks aren't yet supported, but handle this just in case
|
|
if ty.IsMapType() {
|
|
eTy := ty.ElementType()
|
|
for _, eV := range v {
|
|
s.removeAttributes(ctx, eV, eTy)
|
|
}
|
|
return
|
|
}
|
|
|
|
if ty == cty.DynamicPseudoType {
|
|
logging.HelperSchemaDebug(ctx, "ignoring dynamic block", map[string]interface{}{"block": v})
|
|
return
|
|
}
|
|
|
|
if !ty.IsObjectType() {
|
|
// This shouldn't happen, and will fail to decode further on, so
|
|
// there's no need to handle it here.
|
|
logging.HelperSchemaWarn(ctx, "unexpected type for map in JSON state", map[string]interface{}{"type": ty})
|
|
return
|
|
}
|
|
|
|
attrTypes := ty.AttributeTypes()
|
|
for attr, attrV := range v {
|
|
attrTy, ok := attrTypes[attr]
|
|
if !ok {
|
|
logging.HelperSchemaDebug(ctx, "attribute no longer present in schema", map[string]interface{}{"attribute": attr})
|
|
delete(v, attr)
|
|
continue
|
|
}
|
|
|
|
s.removeAttributes(ctx, attrV, attrTy)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *GRPCProviderServer) StopProvider(ctx context.Context, _ *tfprotov5.StopProviderRequest) (*tfprotov5.StopProviderResponse, error) {
|
|
ctx = logging.InitContext(ctx)
|
|
|
|
logging.HelperSchemaTrace(ctx, "Stopping provider")
|
|
|
|
s.stopMu.Lock()
|
|
defer s.stopMu.Unlock()
|
|
|
|
// stop
|
|
close(s.stopCh)
|
|
// reset the stop signal
|
|
s.stopCh = make(chan struct{})
|
|
|
|
logging.HelperSchemaTrace(ctx, "Stopped provider")
|
|
|
|
return &tfprotov5.StopProviderResponse{}, nil
|
|
}
|
|
|
|
func (s *GRPCProviderServer) ConfigureProvider(ctx context.Context, req *tfprotov5.ConfigureProviderRequest) (*tfprotov5.ConfigureProviderResponse, error) {
|
|
ctx = logging.InitContext(ctx)
|
|
resp := &tfprotov5.ConfigureProviderResponse{}
|
|
|
|
schemaBlock := s.getProviderSchemaBlock()
|
|
|
|
configVal, err := msgpack.Unmarshal(req.Config.MsgPack, schemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
s.provider.TerraformVersion = req.TerraformVersion
|
|
|
|
// Ensure there are no nulls that will cause helper/schema to panic.
|
|
if err := validateConfigNulls(ctx, configVal, nil); err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
config := terraform.NewResourceConfigShimmed(configVal, schemaBlock)
|
|
// TODO: remove global stop context hack
|
|
// This attaches a global stop synchro'd context onto the provider.Configure
|
|
// request scoped context. This provides a substitute for the removed provider.StopContext()
|
|
// function. Ideally a provider should migrate to the context aware API that receives
|
|
// request scoped contexts, however this is a large undertaking for very large providers.
|
|
ctxHack := context.WithValue(ctx, StopContextKey, s.StopContext(context.Background()))
|
|
|
|
logging.HelperSchemaTrace(ctx, "Calling downstream")
|
|
diags := s.provider.Configure(ctxHack, config)
|
|
logging.HelperSchemaTrace(ctx, "Called downstream")
|
|
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, diags)
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (s *GRPCProviderServer) ReadResource(ctx context.Context, req *tfprotov5.ReadResourceRequest) (*tfprotov5.ReadResourceResponse, error) {
|
|
ctx = logging.InitContext(ctx)
|
|
resp := &tfprotov5.ReadResourceResponse{
|
|
// helper/schema did previously handle private data during refresh, but
|
|
// core is now going to expect this to be maintained in order to
|
|
// persist it in the state.
|
|
Private: req.Private,
|
|
}
|
|
|
|
res, ok := s.provider.ResourcesMap[req.TypeName]
|
|
if !ok {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, fmt.Errorf("unknown resource type: %s", req.TypeName))
|
|
return resp, nil
|
|
}
|
|
schemaBlock := s.getResourceSchemaBlock(req.TypeName)
|
|
|
|
stateVal, err := msgpack.Unmarshal(req.CurrentState.MsgPack, schemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
instanceState, err := res.ShimInstanceStateFromValue(stateVal)
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
instanceState.RawState = stateVal
|
|
|
|
private := make(map[string]interface{})
|
|
if len(req.Private) > 0 {
|
|
if err := json.Unmarshal(req.Private, &private); err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
}
|
|
instanceState.Meta = private
|
|
|
|
pmSchemaBlock := s.getProviderMetaSchemaBlock()
|
|
if pmSchemaBlock != nil && req.ProviderMeta != nil {
|
|
providerSchemaVal, err := msgpack.Unmarshal(req.ProviderMeta.MsgPack, pmSchemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
instanceState.ProviderMeta = providerSchemaVal
|
|
}
|
|
|
|
newInstanceState, diags := res.RefreshWithoutUpgrade(ctx, instanceState, s.provider.Meta())
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, diags)
|
|
if diags.HasError() {
|
|
return resp, nil
|
|
}
|
|
|
|
if newInstanceState == nil || newInstanceState.ID == "" {
|
|
// The old provider API used an empty id to signal that the remote
|
|
// object appears to have been deleted, but our new protocol expects
|
|
// to see a null value (in the cty sense) in that case.
|
|
newStateMP, err := msgpack.Marshal(cty.NullVal(schemaBlock.ImpliedType()), schemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
}
|
|
resp.NewState = &tfprotov5.DynamicValue{
|
|
MsgPack: newStateMP,
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
// helper/schema should always copy the ID over, but do it again just to be safe
|
|
newInstanceState.Attributes["id"] = newInstanceState.ID
|
|
|
|
newStateVal, err := hcl2shim.HCL2ValueFromFlatmap(newInstanceState.Attributes, schemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
newStateVal = normalizeNullValues(newStateVal, stateVal, false)
|
|
newStateVal = copyTimeoutValues(newStateVal, stateVal)
|
|
|
|
newStateMP, err := msgpack.Marshal(newStateVal, schemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
resp.NewState = &tfprotov5.DynamicValue{
|
|
MsgPack: newStateMP,
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprotov5.PlanResourceChangeRequest) (*tfprotov5.PlanResourceChangeResponse, error) {
|
|
ctx = logging.InitContext(ctx)
|
|
resp := &tfprotov5.PlanResourceChangeResponse{}
|
|
|
|
// This is a signal to Terraform Core that we're doing the best we can to
|
|
// shim the legacy type system of the SDK onto the Terraform type system
|
|
// but we need it to cut us some slack. This setting should not be taken
|
|
// forward to any new SDK implementations, since setting it prevents us
|
|
// from catching certain classes of provider bug that can lead to
|
|
// confusing downstream errors.
|
|
resp.UnsafeToUseLegacyTypeSystem = true //nolint:staticcheck
|
|
|
|
res, ok := s.provider.ResourcesMap[req.TypeName]
|
|
if !ok {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, fmt.Errorf("unknown resource type: %s", req.TypeName))
|
|
return resp, nil
|
|
}
|
|
schemaBlock := s.getResourceSchemaBlock(req.TypeName)
|
|
|
|
priorStateVal, err := msgpack.Unmarshal(req.PriorState.MsgPack, schemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
create := priorStateVal.IsNull()
|
|
|
|
proposedNewStateVal, err := msgpack.Unmarshal(req.ProposedNewState.MsgPack, schemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
// We don't usually plan destroys, but this can return early in any case.
|
|
if proposedNewStateVal.IsNull() {
|
|
resp.PlannedState = req.ProposedNewState
|
|
resp.PlannedPrivate = req.PriorPrivate
|
|
return resp, nil
|
|
}
|
|
|
|
configVal, err := msgpack.Unmarshal(req.Config.MsgPack, schemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
priorState, err := res.ShimInstanceStateFromValue(priorStateVal)
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
priorState.RawState = priorStateVal
|
|
priorState.RawPlan = proposedNewStateVal
|
|
priorState.RawConfig = configVal
|
|
priorPrivate := make(map[string]interface{})
|
|
if len(req.PriorPrivate) > 0 {
|
|
if err := json.Unmarshal(req.PriorPrivate, &priorPrivate); err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
}
|
|
|
|
priorState.Meta = priorPrivate
|
|
|
|
pmSchemaBlock := s.getProviderMetaSchemaBlock()
|
|
if pmSchemaBlock != nil && req.ProviderMeta != nil {
|
|
providerSchemaVal, err := msgpack.Unmarshal(req.ProviderMeta.MsgPack, pmSchemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
priorState.ProviderMeta = providerSchemaVal
|
|
}
|
|
|
|
// Ensure there are no nulls that will cause helper/schema to panic.
|
|
if err := validateConfigNulls(ctx, proposedNewStateVal, nil); err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
// turn the proposed state into a legacy configuration
|
|
cfg := terraform.NewResourceConfigShimmed(proposedNewStateVal, schemaBlock)
|
|
|
|
diff, err := res.SimpleDiff(ctx, priorState, cfg, s.provider.Meta())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
// if this is a new instance, we need to make sure ID is going to be computed
|
|
if create {
|
|
if diff == nil {
|
|
diff = terraform.NewInstanceDiff()
|
|
}
|
|
|
|
diff.Attributes["id"] = &terraform.ResourceAttrDiff{
|
|
NewComputed: true,
|
|
}
|
|
}
|
|
|
|
if diff == nil || len(diff.Attributes) == 0 {
|
|
// schema.Provider.Diff returns nil if it ends up making a diff with no
|
|
// changes, but our new interface wants us to return an actual change
|
|
// description that _shows_ there are no changes. This is always the
|
|
// prior state, because we force a diff above if this is a new instance.
|
|
resp.PlannedState = req.PriorState
|
|
resp.PlannedPrivate = req.PriorPrivate
|
|
return resp, nil
|
|
}
|
|
|
|
if priorState == nil {
|
|
priorState = &terraform.InstanceState{}
|
|
}
|
|
|
|
// now we need to apply the diff to the prior state, so get the planned state
|
|
plannedAttrs, err := diff.Apply(priorState.Attributes, schemaBlock)
|
|
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
plannedStateVal, err := hcl2shim.HCL2ValueFromFlatmap(plannedAttrs, schemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
plannedStateVal, err = schemaBlock.CoerceValue(plannedStateVal)
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
plannedStateVal = normalizeNullValues(plannedStateVal, proposedNewStateVal, false)
|
|
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
plannedStateVal = copyTimeoutValues(plannedStateVal, proposedNewStateVal)
|
|
|
|
// The old SDK code has some imprecisions that cause it to sometimes
|
|
// generate differences that the SDK itself does not consider significant
|
|
// but Terraform Core would. To avoid producing weird do-nothing diffs
|
|
// in that case, we'll check if the provider as produced something we
|
|
// think is "equivalent" to the prior state and just return the prior state
|
|
// itself if so, thus ensuring that Terraform Core will treat this as
|
|
// a no-op. See the docs for ValuesSDKEquivalent for some caveats on its
|
|
// accuracy.
|
|
forceNoChanges := false
|
|
if hcl2shim.ValuesSDKEquivalent(priorStateVal, plannedStateVal) {
|
|
plannedStateVal = priorStateVal
|
|
forceNoChanges = true
|
|
}
|
|
|
|
// if this was creating the resource, we need to set any remaining computed
|
|
// fields
|
|
if create {
|
|
plannedStateVal = SetUnknowns(plannedStateVal, schemaBlock)
|
|
}
|
|
|
|
plannedMP, err := msgpack.Marshal(plannedStateVal, schemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
resp.PlannedState = &tfprotov5.DynamicValue{
|
|
MsgPack: plannedMP,
|
|
}
|
|
|
|
// encode any timeouts into the diff Meta
|
|
t := &ResourceTimeout{}
|
|
if err := t.ConfigDecode(res, cfg); err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
if err := t.DiffEncode(diff); err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
// Now we need to store any NewExtra values, which are where any actual
|
|
// StateFunc modified config fields are hidden.
|
|
privateMap := diff.Meta
|
|
if privateMap == nil {
|
|
privateMap = map[string]interface{}{}
|
|
}
|
|
|
|
newExtra := map[string]interface{}{}
|
|
|
|
for k, v := range diff.Attributes {
|
|
if v.NewExtra != nil {
|
|
newExtra[k] = v.NewExtra
|
|
}
|
|
}
|
|
privateMap[newExtraKey] = newExtra
|
|
|
|
// the Meta field gets encoded into PlannedPrivate
|
|
plannedPrivate, err := json.Marshal(privateMap)
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
resp.PlannedPrivate = plannedPrivate
|
|
|
|
// collect the attributes that require instance replacement, and convert
|
|
// them to cty.Paths.
|
|
var requiresNew []string
|
|
if !forceNoChanges {
|
|
for attr, d := range diff.Attributes {
|
|
if d.RequiresNew {
|
|
requiresNew = append(requiresNew, attr)
|
|
}
|
|
}
|
|
}
|
|
|
|
// If anything requires a new resource already, or the "id" field indicates
|
|
// that we will be creating a new resource, then we need to add that to
|
|
// RequiresReplace so that core can tell if the instance is being replaced
|
|
// even if changes are being suppressed via "ignore_changes".
|
|
id := plannedStateVal.GetAttr("id")
|
|
if len(requiresNew) > 0 || id.IsNull() || !id.IsKnown() {
|
|
requiresNew = append(requiresNew, "id")
|
|
}
|
|
|
|
requiresReplace, err := hcl2shim.RequiresReplace(requiresNew, schemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
// convert these to the protocol structures
|
|
for _, p := range requiresReplace {
|
|
resp.RequiresReplace = append(resp.RequiresReplace, pathToAttributePath(p))
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfprotov5.ApplyResourceChangeRequest) (*tfprotov5.ApplyResourceChangeResponse, error) {
|
|
ctx = logging.InitContext(ctx)
|
|
resp := &tfprotov5.ApplyResourceChangeResponse{
|
|
// Start with the existing state as a fallback
|
|
NewState: req.PriorState,
|
|
}
|
|
|
|
res, ok := s.provider.ResourcesMap[req.TypeName]
|
|
if !ok {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, fmt.Errorf("unknown resource type: %s", req.TypeName))
|
|
return resp, nil
|
|
}
|
|
schemaBlock := s.getResourceSchemaBlock(req.TypeName)
|
|
|
|
priorStateVal, err := msgpack.Unmarshal(req.PriorState.MsgPack, schemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
plannedStateVal, err := msgpack.Unmarshal(req.PlannedState.MsgPack, schemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
configVal, err := msgpack.Unmarshal(req.Config.MsgPack, schemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
priorState, err := res.ShimInstanceStateFromValue(priorStateVal)
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
private := make(map[string]interface{})
|
|
if len(req.PlannedPrivate) > 0 {
|
|
if err := json.Unmarshal(req.PlannedPrivate, &private); err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
}
|
|
|
|
var diff *terraform.InstanceDiff
|
|
destroy := false
|
|
|
|
// a null state means we are destroying the instance
|
|
if plannedStateVal.IsNull() {
|
|
destroy = true
|
|
diff = &terraform.InstanceDiff{
|
|
Attributes: make(map[string]*terraform.ResourceAttrDiff),
|
|
Meta: make(map[string]interface{}),
|
|
Destroy: true,
|
|
RawPlan: plannedStateVal,
|
|
RawState: priorStateVal,
|
|
RawConfig: configVal,
|
|
}
|
|
} else {
|
|
diff, err = DiffFromValues(ctx, priorStateVal, plannedStateVal, configVal, stripResourceModifiers(res))
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
}
|
|
|
|
if diff == nil {
|
|
diff = &terraform.InstanceDiff{
|
|
Attributes: make(map[string]*terraform.ResourceAttrDiff),
|
|
Meta: make(map[string]interface{}),
|
|
RawPlan: plannedStateVal,
|
|
RawState: priorStateVal,
|
|
RawConfig: configVal,
|
|
}
|
|
}
|
|
|
|
// add NewExtra Fields that may have been stored in the private data
|
|
if newExtra := private[newExtraKey]; newExtra != nil {
|
|
for k, v := range newExtra.(map[string]interface{}) {
|
|
d := diff.Attributes[k]
|
|
|
|
if d == nil {
|
|
d = &terraform.ResourceAttrDiff{}
|
|
}
|
|
|
|
d.NewExtra = v
|
|
diff.Attributes[k] = d
|
|
}
|
|
}
|
|
|
|
if private != nil {
|
|
diff.Meta = private
|
|
}
|
|
|
|
for k, d := range diff.Attributes {
|
|
// We need to turn off any RequiresNew. There could be attributes
|
|
// without changes in here inserted by helper/schema, but if they have
|
|
// RequiresNew then the state will be dropped from the ResourceData.
|
|
d.RequiresNew = false
|
|
|
|
// Check that any "removed" attributes that don't actually exist in the
|
|
// prior state, or helper/schema will confuse itself
|
|
if d.NewRemoved {
|
|
if _, ok := priorState.Attributes[k]; !ok {
|
|
delete(diff.Attributes, k)
|
|
}
|
|
}
|
|
}
|
|
|
|
pmSchemaBlock := s.getProviderMetaSchemaBlock()
|
|
if pmSchemaBlock != nil && req.ProviderMeta != nil {
|
|
providerSchemaVal, err := msgpack.Unmarshal(req.ProviderMeta.MsgPack, pmSchemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
priorState.ProviderMeta = providerSchemaVal
|
|
}
|
|
|
|
newInstanceState, diags := res.Apply(ctx, priorState, diff, s.provider.Meta())
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, diags)
|
|
|
|
newStateVal := cty.NullVal(schemaBlock.ImpliedType())
|
|
|
|
// Always return a null value for destroy.
|
|
// While this is usually indicated by a nil state, check for missing ID or
|
|
// attributes in the case of a provider failure.
|
|
if destroy || newInstanceState == nil || newInstanceState.Attributes == nil || newInstanceState.ID == "" {
|
|
newStateMP, err := msgpack.Marshal(newStateVal, schemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
resp.NewState = &tfprotov5.DynamicValue{
|
|
MsgPack: newStateMP,
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
// We keep the null val if we destroyed the resource, otherwise build the
|
|
// entire object, even if the new state was nil.
|
|
newStateVal, err = StateValueFromInstanceState(newInstanceState, schemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
newStateVal = normalizeNullValues(newStateVal, plannedStateVal, true)
|
|
|
|
newStateVal = copyTimeoutValues(newStateVal, plannedStateVal)
|
|
|
|
newStateMP, err := msgpack.Marshal(newStateVal, schemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
resp.NewState = &tfprotov5.DynamicValue{
|
|
MsgPack: newStateMP,
|
|
}
|
|
|
|
meta, err := json.Marshal(newInstanceState.Meta)
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
resp.Private = meta
|
|
|
|
// This is a signal to Terraform Core that we're doing the best we can to
|
|
// shim the legacy type system of the SDK onto the Terraform type system
|
|
// but we need it to cut us some slack. This setting should not be taken
|
|
// forward to any new SDK implementations, since setting it prevents us
|
|
// from catching certain classes of provider bug that can lead to
|
|
// confusing downstream errors.
|
|
resp.UnsafeToUseLegacyTypeSystem = true //nolint:staticcheck
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (s *GRPCProviderServer) ImportResourceState(ctx context.Context, req *tfprotov5.ImportResourceStateRequest) (*tfprotov5.ImportResourceStateResponse, error) {
|
|
ctx = logging.InitContext(ctx)
|
|
resp := &tfprotov5.ImportResourceStateResponse{}
|
|
|
|
info := &terraform.InstanceInfo{
|
|
Type: req.TypeName,
|
|
}
|
|
|
|
newInstanceStates, err := s.provider.ImportState(ctx, info, req.ID)
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
for _, is := range newInstanceStates {
|
|
// copy the ID again just to be sure it wasn't missed
|
|
is.Attributes["id"] = is.ID
|
|
|
|
resourceType := is.Ephemeral.Type
|
|
if resourceType == "" {
|
|
resourceType = req.TypeName
|
|
}
|
|
|
|
schemaBlock := s.getResourceSchemaBlock(resourceType)
|
|
newStateVal, err := hcl2shim.HCL2ValueFromFlatmap(is.Attributes, schemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
// Normalize the value and fill in any missing blocks.
|
|
newStateVal = objchange.NormalizeObjectFromLegacySDK(newStateVal, schemaBlock)
|
|
|
|
newStateMP, err := msgpack.Marshal(newStateVal, schemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
meta, err := json.Marshal(is.Meta)
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
importedResource := &tfprotov5.ImportedResource{
|
|
TypeName: resourceType,
|
|
State: &tfprotov5.DynamicValue{
|
|
MsgPack: newStateMP,
|
|
},
|
|
Private: meta,
|
|
}
|
|
|
|
resp.ImportedResources = append(resp.ImportedResources, importedResource)
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (s *GRPCProviderServer) ReadDataSource(ctx context.Context, req *tfprotov5.ReadDataSourceRequest) (*tfprotov5.ReadDataSourceResponse, error) {
|
|
ctx = logging.InitContext(ctx)
|
|
resp := &tfprotov5.ReadDataSourceResponse{}
|
|
|
|
schemaBlock := s.getDatasourceSchemaBlock(req.TypeName)
|
|
|
|
configVal, err := msgpack.Unmarshal(req.Config.MsgPack, schemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
// Ensure there are no nulls that will cause helper/schema to panic.
|
|
if err := validateConfigNulls(ctx, configVal, nil); err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
config := terraform.NewResourceConfigShimmed(configVal, schemaBlock)
|
|
|
|
// we need to still build the diff separately with the Read method to match
|
|
// the old behavior
|
|
res, ok := s.provider.DataSourcesMap[req.TypeName]
|
|
if !ok {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, fmt.Errorf("unknown data source: %s", req.TypeName))
|
|
return resp, nil
|
|
}
|
|
diff, err := res.Diff(ctx, nil, config, s.provider.Meta())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
// Not setting RawConfig here is okay, as ResourceData.GetRawConfig()
|
|
// will return a NullVal of the schema if there is no InstanceDiff.
|
|
if diff != nil {
|
|
diff.RawConfig = configVal
|
|
}
|
|
|
|
// now we can get the new complete data source
|
|
newInstanceState, diags := res.ReadDataApply(ctx, diff, s.provider.Meta())
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, diags)
|
|
if diags.HasError() {
|
|
return resp, nil
|
|
}
|
|
|
|
newStateVal, err := StateValueFromInstanceState(newInstanceState, schemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
|
|
newStateVal = copyTimeoutValues(newStateVal, configVal)
|
|
|
|
newStateMP, err := msgpack.Marshal(newStateVal, schemaBlock.ImpliedType())
|
|
if err != nil {
|
|
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
|
|
return resp, nil
|
|
}
|
|
resp.State = &tfprotov5.DynamicValue{
|
|
MsgPack: newStateMP,
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func pathToAttributePath(path cty.Path) *tftypes.AttributePath {
|
|
var steps []tftypes.AttributePathStep
|
|
|
|
for _, step := range path {
|
|
switch s := step.(type) {
|
|
case cty.GetAttrStep:
|
|
steps = append(steps,
|
|
tftypes.AttributeName(s.Name),
|
|
)
|
|
case cty.IndexStep:
|
|
ty := s.Key.Type()
|
|
switch ty {
|
|
case cty.Number:
|
|
i, _ := s.Key.AsBigFloat().Int64()
|
|
steps = append(steps,
|
|
tftypes.ElementKeyInt(i),
|
|
)
|
|
case cty.String:
|
|
steps = append(steps,
|
|
tftypes.ElementKeyString(s.Key.AsString()),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(steps) < 1 {
|
|
return nil
|
|
}
|
|
return tftypes.NewAttributePathWithSteps(steps)
|
|
}
|
|
|
|
// helper/schema throws away timeout values from the config and stores them in
|
|
// the Private/Meta fields. we need to copy those values into the planned state
|
|
// so that core doesn't see a perpetual diff with the timeout block.
|
|
func copyTimeoutValues(to cty.Value, from cty.Value) cty.Value {
|
|
// if `to` is null we are planning to remove it altogether.
|
|
if to.IsNull() {
|
|
return to
|
|
}
|
|
toAttrs := to.AsValueMap()
|
|
// We need to remove the key since the hcl2shims will add a non-null block
|
|
// because we can't determine if a single block was null from the flatmapped
|
|
// values. This needs to conform to the correct schema for marshaling, so
|
|
// change the value to null rather than deleting it from the object map.
|
|
timeouts, ok := toAttrs[TimeoutsConfigKey]
|
|
if ok {
|
|
toAttrs[TimeoutsConfigKey] = cty.NullVal(timeouts.Type())
|
|
}
|
|
|
|
// if from is null then there are no timeouts to copy
|
|
if from.IsNull() {
|
|
return cty.ObjectVal(toAttrs)
|
|
}
|
|
|
|
fromAttrs := from.AsValueMap()
|
|
timeouts, ok = fromAttrs[TimeoutsConfigKey]
|
|
|
|
// timeouts shouldn't be unknown, but don't copy possibly invalid values either
|
|
if !ok || timeouts.IsNull() || !timeouts.IsWhollyKnown() {
|
|
// no timeouts block to copy
|
|
return cty.ObjectVal(toAttrs)
|
|
}
|
|
|
|
toAttrs[TimeoutsConfigKey] = timeouts
|
|
|
|
return cty.ObjectVal(toAttrs)
|
|
}
|
|
|
|
// stripResourceModifiers takes a *schema.Resource and returns a deep copy with all
|
|
// StateFuncs and CustomizeDiffs removed. This will be used during apply to
|
|
// create a diff from a planned state where the diff modifications have already
|
|
// been applied.
|
|
func stripResourceModifiers(r *Resource) *Resource {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
// start with a shallow copy
|
|
newResource := new(Resource)
|
|
*newResource = *r
|
|
|
|
newResource.CustomizeDiff = nil
|
|
newResource.Schema = map[string]*Schema{}
|
|
|
|
for k, s := range r.Schema {
|
|
newResource.Schema[k] = stripSchema(s)
|
|
}
|
|
|
|
return newResource
|
|
}
|
|
|
|
func stripSchema(s *Schema) *Schema {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
// start with a shallow copy
|
|
newSchema := new(Schema)
|
|
*newSchema = *s
|
|
|
|
newSchema.StateFunc = nil
|
|
|
|
switch e := newSchema.Elem.(type) {
|
|
case *Schema:
|
|
newSchema.Elem = stripSchema(e)
|
|
case *Resource:
|
|
newSchema.Elem = stripResourceModifiers(e)
|
|
}
|
|
|
|
return newSchema
|
|
}
|
|
|
|
// Zero values and empty containers may be interchanged by the apply process.
|
|
// When there is a discrepency between src and dst value being null or empty,
|
|
// prefer the src value. This takes a little more liberty with set types, since
|
|
// we can't correlate modified set values. In the case of sets, if the src set
|
|
// was wholly known we assume the value was correctly applied and copy that
|
|
// entirely to the new value.
|
|
// While apply prefers the src value, during plan we prefer dst whenever there
|
|
// is an unknown or a set is involved, since the plan can alter the value
|
|
// however it sees fit. This however means that a CustomizeDiffFunction may not
|
|
// be able to change a null to an empty value or vice versa, but that should be
|
|
// very uncommon nor was it reliable before 0.12 either.
|
|
func normalizeNullValues(dst, src cty.Value, apply bool) cty.Value {
|
|
ty := dst.Type()
|
|
if !src.IsNull() && !src.IsKnown() {
|
|
// Return src during plan to retain unknown interpolated placeholders,
|
|
// which could be lost if we're only updating a resource. If this is a
|
|
// read scenario, then there shouldn't be any unknowns at all.
|
|
if dst.IsNull() && !apply {
|
|
return src
|
|
}
|
|
return dst
|
|
}
|
|
|
|
// Handle null/empty changes for collections during apply.
|
|
// A change between null and empty values prefers src to make sure the state
|
|
// is consistent between plan and apply.
|
|
if ty.IsCollectionType() && apply {
|
|
dstEmpty := !dst.IsNull() && dst.IsKnown() && dst.LengthInt() == 0
|
|
srcEmpty := !src.IsNull() && src.IsKnown() && src.LengthInt() == 0
|
|
|
|
if (src.IsNull() && dstEmpty) || (srcEmpty && dst.IsNull()) {
|
|
return src
|
|
}
|
|
}
|
|
|
|
// check the invariants that we need below, to ensure we are working with
|
|
// non-null and known values.
|
|
if src.IsNull() || !src.IsKnown() || !dst.IsKnown() {
|
|
return dst
|
|
}
|
|
|
|
switch {
|
|
case ty.IsMapType(), ty.IsObjectType():
|
|
var dstMap map[string]cty.Value
|
|
if !dst.IsNull() {
|
|
dstMap = dst.AsValueMap()
|
|
}
|
|
if dstMap == nil {
|
|
dstMap = map[string]cty.Value{}
|
|
}
|
|
|
|
srcMap := src.AsValueMap()
|
|
for key, v := range srcMap {
|
|
dstVal, ok := dstMap[key]
|
|
if !ok && apply && ty.IsMapType() {
|
|
// don't transfer old map values to dst during apply
|
|
continue
|
|
}
|
|
|
|
if dstVal == cty.NilVal {
|
|
if !apply && ty.IsMapType() {
|
|
// let plan shape this map however it wants
|
|
continue
|
|
}
|
|
dstVal = cty.NullVal(v.Type())
|
|
}
|
|
|
|
dstMap[key] = normalizeNullValues(dstVal, v, apply)
|
|
}
|
|
|
|
// you can't call MapVal/ObjectVal with empty maps, but nothing was
|
|
// copied in anyway. If the dst is nil, and the src is known, assume the
|
|
// src is correct.
|
|
if len(dstMap) == 0 {
|
|
if dst.IsNull() && src.IsWhollyKnown() && apply {
|
|
return src
|
|
}
|
|
return dst
|
|
}
|
|
|
|
if ty.IsMapType() {
|
|
// helper/schema will populate an optional+computed map with
|
|
// unknowns which we have to fixup here.
|
|
// It would be preferable to simply prevent any known value from
|
|
// becoming unknown, but concessions have to be made to retain the
|
|
// broken legacy behavior when possible.
|
|
for k, srcVal := range srcMap {
|
|
if !srcVal.IsNull() && srcVal.IsKnown() {
|
|
dstVal, ok := dstMap[k]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
if !dstVal.IsNull() && !dstVal.IsKnown() {
|
|
dstMap[k] = srcVal
|
|
}
|
|
}
|
|
}
|
|
|
|
return cty.MapVal(dstMap)
|
|
}
|
|
|
|
return cty.ObjectVal(dstMap)
|
|
|
|
case ty.IsSetType():
|
|
// If the original was wholly known, then we expect that is what the
|
|
// provider applied. The apply process loses too much information to
|
|
// reliably re-create the set.
|
|
if src.IsWhollyKnown() && apply {
|
|
return src
|
|
}
|
|
|
|
case ty.IsListType(), ty.IsTupleType():
|
|
// If the dst is null, and the src is known, then we lost an empty value
|
|
// so take the original.
|
|
if dst.IsNull() {
|
|
if src.IsWhollyKnown() && src.LengthInt() == 0 && apply {
|
|
return src
|
|
}
|
|
|
|
// if dst is null and src only contains unknown values, then we lost
|
|
// those during a read or plan.
|
|
if !apply && !src.IsNull() {
|
|
allUnknown := true
|
|
for _, v := range src.AsValueSlice() {
|
|
if v.IsKnown() {
|
|
allUnknown = false
|
|
break
|
|
}
|
|
}
|
|
if allUnknown {
|
|
return src
|
|
}
|
|
}
|
|
|
|
return dst
|
|
}
|
|
|
|
// if the lengths are identical, then iterate over each element in succession.
|
|
srcLen := src.LengthInt()
|
|
dstLen := dst.LengthInt()
|
|
if srcLen == dstLen && srcLen > 0 {
|
|
srcs := src.AsValueSlice()
|
|
dsts := dst.AsValueSlice()
|
|
|
|
for i := 0; i < srcLen; i++ {
|
|
dsts[i] = normalizeNullValues(dsts[i], srcs[i], apply)
|
|
}
|
|
|
|
if ty.IsTupleType() {
|
|
return cty.TupleVal(dsts)
|
|
}
|
|
return cty.ListVal(dsts)
|
|
}
|
|
|
|
case ty == cty.String:
|
|
// The legacy SDK should not be able to remove a value during plan or
|
|
// apply, however we are only going to overwrite this if the source was
|
|
// an empty string, since that is what is often equated with unset and
|
|
// lost in the diff process.
|
|
if dst.IsNull() && src.AsString() == "" {
|
|
return src
|
|
}
|
|
}
|
|
|
|
return dst
|
|
}
|
|
|
|
// validateConfigNulls checks a config value for unsupported nulls before
|
|
// attempting to shim the value. While null values can mostly be ignored in the
|
|
// configuration, since they're not supported in HCL1, the case where a null
|
|
// appears in a list-like attribute (list, set, tuple) will present a nil value
|
|
// to helper/schema which can panic. Return an error to the user in this case,
|
|
// indicating the attribute with the null value.
|
|
func validateConfigNulls(ctx context.Context, v cty.Value, path cty.Path) []*tfprotov5.Diagnostic {
|
|
var diags []*tfprotov5.Diagnostic
|
|
if v.IsNull() || !v.IsKnown() {
|
|
return diags
|
|
}
|
|
|
|
switch {
|
|
case v.Type().IsListType() || v.Type().IsSetType() || v.Type().IsTupleType():
|
|
it := v.ElementIterator()
|
|
for it.Next() {
|
|
kv, ev := it.Element()
|
|
if ev.IsNull() {
|
|
// if this is a set, the kv is also going to be null which
|
|
// isn't a valid path element, so we can't append it to the
|
|
// diagnostic.
|
|
p := path
|
|
if !kv.IsNull() {
|
|
p = append(p, cty.IndexStep{Key: kv})
|
|
}
|
|
|
|
diags = append(diags, &tfprotov5.Diagnostic{
|
|
Severity: tfprotov5.DiagnosticSeverityError,
|
|
Summary: "Null value found in list",
|
|
Detail: "Null values are not allowed for this attribute value.",
|
|
Attribute: convert.PathToAttributePath(p),
|
|
})
|
|
continue
|
|
}
|
|
|
|
d := validateConfigNulls(ctx, ev, append(path, cty.IndexStep{Key: kv}))
|
|
diags = convert.AppendProtoDiag(ctx, diags, d)
|
|
}
|
|
|
|
case v.Type().IsMapType() || v.Type().IsObjectType():
|
|
it := v.ElementIterator()
|
|
for it.Next() {
|
|
kv, ev := it.Element()
|
|
var step cty.PathStep
|
|
switch {
|
|
case v.Type().IsMapType():
|
|
step = cty.IndexStep{Key: kv}
|
|
case v.Type().IsObjectType():
|
|
step = cty.GetAttrStep{Name: kv.AsString()}
|
|
}
|
|
d := validateConfigNulls(ctx, ev, append(path, step))
|
|
diags = convert.AppendProtoDiag(ctx, diags, d)
|
|
}
|
|
}
|
|
|
|
return diags
|
|
}
|