diff --git a/helper/plugin/doc.go b/helper/plugin/doc.go deleted file mode 100644 index 82b5937bf..000000000 --- a/helper/plugin/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Package plugin contains types and functions to help Terraform plugins -// implement the plugin rpc interface. -// The primary Provider type will be responsible for converting from the grpc -// wire protocol to the types and methods known to the provider -// implementations. -package plugin diff --git a/helper/plugin/grpc_provider.go b/helper/plugin/grpc_provider.go deleted file mode 100644 index 06ebaf421..000000000 --- a/helper/plugin/grpc_provider.go +++ /dev/null @@ -1,1436 +0,0 @@ -package plugin - -import ( - "encoding/json" - "fmt" - "log" - "strconv" - - "github.com/zclconf/go-cty/cty" - ctyconvert "github.com/zclconf/go-cty/cty/convert" - "github.com/zclconf/go-cty/cty/msgpack" - context "golang.org/x/net/context" - - "github.com/hashicorp/terraform/configs/configschema" - "github.com/hashicorp/terraform/configs/hcl2shim" - "github.com/hashicorp/terraform/helper/schema" - proto "github.com/hashicorp/terraform/internal/tfplugin5" - "github.com/hashicorp/terraform/plans/objchange" - "github.com/hashicorp/terraform/plugin/convert" - "github.com/hashicorp/terraform/terraform" -) - -const newExtraKey = "_new_extra_shim" - -// NewGRPCProviderServerShim wraps a terraform.ResourceProvider in a -// proto.ProviderServer implementation. If the provided provider is not a -// *schema.Provider, this will return nil, -func NewGRPCProviderServerShim(p terraform.ResourceProvider) *GRPCProviderServer { - sp, ok := p.(*schema.Provider) - if !ok { - return nil - } - - return &GRPCProviderServer{ - provider: sp, - } -} - -// GRPCProviderServer handles the server, or plugin side of the rpc connection. -type GRPCProviderServer struct { - provider *schema.Provider -} - -func (s *GRPCProviderServer) GetSchema(_ context.Context, req *proto.GetProviderSchema_Request) (*proto.GetProviderSchema_Response, error) { - // Here we are certain that the provider is being called through grpc, so - // make sure the feature flag for helper/schema is set - schema.SetProto5() - - resp := &proto.GetProviderSchema_Response{ - ResourceSchemas: make(map[string]*proto.Schema), - DataSourceSchemas: make(map[string]*proto.Schema), - } - - resp.Provider = &proto.Schema{ - Block: convert.ConfigSchemaToProto(s.getProviderSchemaBlock()), - } - - resp.ProviderMeta = &proto.Schema{ - Block: convert.ConfigSchemaToProto(s.getProviderMetaSchemaBlock()), - } - - for typ, res := range s.provider.ResourcesMap { - resp.ResourceSchemas[typ] = &proto.Schema{ - Version: int64(res.SchemaVersion), - Block: convert.ConfigSchemaToProto(res.CoreConfigSchema()), - } - } - - for typ, dat := range s.provider.DataSourcesMap { - resp.DataSourceSchemas[typ] = &proto.Schema{ - Version: int64(dat.SchemaVersion), - Block: convert.ConfigSchemaToProto(dat.CoreConfigSchema()), - } - } - - return resp, nil -} - -func (s *GRPCProviderServer) getProviderSchemaBlock() *configschema.Block { - return schema.InternalMap(s.provider.Schema).CoreConfigSchema() -} - -func (s *GRPCProviderServer) getProviderMetaSchemaBlock() *configschema.Block { - return schema.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(_ context.Context, req *proto.PrepareProviderConfig_Request) (*proto.PrepareProviderConfig_Response, error) { - resp := &proto.PrepareProviderConfig_Response{} - - schemaBlock := s.getProviderSchemaBlock() - - configVal, err := msgpack.Unmarshal(req.Config.Msgpack, schemaBlock.ImpliedType()) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(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 != "" || attrSchema.Removed != "" { - return val, nil - } - - // find a default value if it exists - def, err := attrSchema.DefaultValue() - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, fmt.Errorf("error getting default for %q: %s", getAttr.Name, err)) - return val, 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(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 { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, fmt.Errorf("error setting default for %q: %s", getAttr.Name, err)) - } - - return val, err - }) - if err != nil { - // any error here was already added to the diagnostics - return resp, nil - } - - configVal, err = schemaBlock.CoerceValue(configVal) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - return resp, nil - } - - // Ensure there are no nulls that will cause helper/schema to panic. - if err := validateConfigNulls(configVal, nil); err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - return resp, nil - } - - config := terraform.NewResourceConfigShimmed(configVal, schemaBlock) - - warns, errs := s.provider.Validate(config) - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, convert.WarnsAndErrsToProto(warns, errs)) - - preparedConfigMP, err := msgpack.Marshal(configVal, schemaBlock.ImpliedType()) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - return resp, nil - } - - resp.PreparedConfig = &proto.DynamicValue{Msgpack: preparedConfigMP} - - return resp, nil -} - -func (s *GRPCProviderServer) ValidateResourceTypeConfig(_ context.Context, req *proto.ValidateResourceTypeConfig_Request) (*proto.ValidateResourceTypeConfig_Response, error) { - resp := &proto.ValidateResourceTypeConfig_Response{} - - schemaBlock := s.getResourceSchemaBlock(req.TypeName) - - configVal, err := msgpack.Unmarshal(req.Config.Msgpack, schemaBlock.ImpliedType()) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - return resp, nil - } - - config := terraform.NewResourceConfigShimmed(configVal, schemaBlock) - - warns, errs := s.provider.ValidateResource(req.TypeName, config) - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, convert.WarnsAndErrsToProto(warns, errs)) - - return resp, nil -} - -func (s *GRPCProviderServer) ValidateDataSourceConfig(_ context.Context, req *proto.ValidateDataSourceConfig_Request) (*proto.ValidateDataSourceConfig_Response, error) { - resp := &proto.ValidateDataSourceConfig_Response{} - - schemaBlock := s.getDatasourceSchemaBlock(req.TypeName) - - configVal, err := msgpack.Unmarshal(req.Config.Msgpack, schemaBlock.ImpliedType()) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - return resp, nil - } - - // Ensure there are no nulls that will cause helper/schema to panic. - if err := validateConfigNulls(configVal, nil); err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - return resp, nil - } - - config := terraform.NewResourceConfigShimmed(configVal, schemaBlock) - - warns, errs := s.provider.ValidateDataSource(req.TypeName, config) - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, convert.WarnsAndErrsToProto(warns, errs)) - - return resp, nil -} - -func (s *GRPCProviderServer) UpgradeResourceState(_ context.Context, req *proto.UpgradeResourceState_Request) (*proto.UpgradeResourceState_Response, error) { - resp := &proto.UpgradeResourceState_Response{} - - res := s.provider.ResourcesMap[req.TypeName] - 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: - jsonMap, version, err = s.upgradeFlatmapState(version, req.RawState.Flatmap, res) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - return resp, nil - } - // if there's a JSON state, we need to decode it. - case len(req.RawState.Json) > 0: - err = json.Unmarshal(req.RawState.Json, &jsonMap) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - return resp, nil - } - default: - log.Println("[DEBUG] no state provided to upgrade") - return resp, nil - } - - // complete the upgrade of the JSON states - jsonMap, err = s.upgradeJSONState(version, jsonMap, res) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - return resp, nil - } - - // The provider isn't required to clean out removed fields - s.removeAttributes(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 := schema.JSONMapToStateValue(jsonMap, schemaBlock) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(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(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(resp.Diagnostics, err) - return resp, nil - } - - resp.UpgradedState = &proto.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(version int, m map[string]string, res *schema.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 := schema.StateValueToJSONMap(newConfigVal, schemaType) - return jsonMap, upgradedVersion, err -} - -func (s *GRPCProviderServer) upgradeJSONState(version int, m map[string]interface{}, res *schema.Resource) (map[string]interface{}, error) { - var err error - - for _, upgrader := range res.StateUpgraders { - if version != upgrader.Version { - continue - } - - m, err = upgrader.Upgrade(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(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(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(eV, eTy) - } - return - } - - if ty == cty.DynamicPseudoType { - log.Printf("[DEBUG] ignoring dynamic block: %#v\n", 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. - log.Printf("[WARN] unexpected type %#v for map in json state", ty) - return - } - - attrTypes := ty.AttributeTypes() - for attr, attrV := range v { - attrTy, ok := attrTypes[attr] - if !ok { - log.Printf("[DEBUG] attribute %q no longer present in schema", attr) - delete(v, attr) - continue - } - - s.removeAttributes(attrV, attrTy) - } - } -} - -func (s *GRPCProviderServer) Stop(_ context.Context, _ *proto.Stop_Request) (*proto.Stop_Response, error) { - resp := &proto.Stop_Response{} - - err := s.provider.Stop() - if err != nil { - resp.Error = err.Error() - } - - return resp, nil -} - -func (s *GRPCProviderServer) Configure(_ context.Context, req *proto.Configure_Request) (*proto.Configure_Response, error) { - resp := &proto.Configure_Response{} - - schemaBlock := s.getProviderSchemaBlock() - - configVal, err := msgpack.Unmarshal(req.Config.Msgpack, schemaBlock.ImpliedType()) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(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(configVal, nil); err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - return resp, nil - } - - config := terraform.NewResourceConfigShimmed(configVal, schemaBlock) - err = s.provider.Configure(config) - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - - return resp, nil -} - -func (s *GRPCProviderServer) ReadResource(_ context.Context, req *proto.ReadResource_Request) (*proto.ReadResource_Response, error) { - resp := &proto.ReadResource_Response{ - // 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 := s.provider.ResourcesMap[req.TypeName] - schemaBlock := s.getResourceSchemaBlock(req.TypeName) - - stateVal, err := msgpack.Unmarshal(req.CurrentState.Msgpack, schemaBlock.ImpliedType()) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - return resp, nil - } - - instanceState, err := res.ShimInstanceStateFromValue(stateVal) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - return resp, nil - } - - private := make(map[string]interface{}) - if len(req.Private) > 0 { - if err := json.Unmarshal(req.Private, &private); err != nil { - resp.Diagnostics = convert.AppendProtoDiag(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(resp.Diagnostics, err) - return resp, nil - } - instanceState.ProviderMeta = providerSchemaVal - } - - newInstanceState, err := res.RefreshWithoutUpgrade(instanceState, s.provider.Meta()) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - 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(resp.Diagnostics, err) - } - resp.NewState = &proto.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(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(resp.Diagnostics, err) - return resp, nil - } - - resp.NewState = &proto.DynamicValue{ - Msgpack: newStateMP, - } - - return resp, nil -} - -func (s *GRPCProviderServer) PlanResourceChange(_ context.Context, req *proto.PlanResourceChange_Request) (*proto.PlanResourceChange_Response, error) { - resp := &proto.PlanResourceChange_Response{} - - // 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.LegacyTypeSystem = true - - res := s.provider.ResourcesMap[req.TypeName] - schemaBlock := s.getResourceSchemaBlock(req.TypeName) - - priorStateVal, err := msgpack.Unmarshal(req.PriorState.Msgpack, schemaBlock.ImpliedType()) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(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(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 - } - - info := &terraform.InstanceInfo{ - Type: req.TypeName, - } - - priorState, err := res.ShimInstanceStateFromValue(priorStateVal) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - return resp, nil - } - priorPrivate := make(map[string]interface{}) - if len(req.PriorPrivate) > 0 { - if err := json.Unmarshal(req.PriorPrivate, &priorPrivate); err != nil { - resp.Diagnostics = convert.AppendProtoDiag(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(resp.Diagnostics, err) - return resp, nil - } - priorState.ProviderMeta = providerSchemaVal - } - - // Ensure there are no nulls that will cause helper/schema to panic. - if err := validateConfigNulls(proposedNewStateVal, nil); err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - return resp, nil - } - - // turn the proposed state into a legacy configuration - cfg := terraform.NewResourceConfigShimmed(proposedNewStateVal, schemaBlock) - - diff, err := s.provider.SimpleDiff(info, priorState, cfg) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(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) - - plannedStateVal, err := hcl2shim.HCL2ValueFromFlatmap(plannedAttrs, schemaBlock.ImpliedType()) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - return resp, nil - } - - plannedStateVal, err = schemaBlock.CoerceValue(plannedStateVal) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - return resp, nil - } - - plannedStateVal = normalizeNullValues(plannedStateVal, proposedNewStateVal, false) - - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(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(resp.Diagnostics, err) - return resp, nil - } - resp.PlannedState = &proto.DynamicValue{ - Msgpack: plannedMP, - } - - // encode any timeouts into the diff Meta - t := &schema.ResourceTimeout{} - if err := t.ConfigDecode(res, cfg); err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - return resp, nil - } - - if err := t.DiffEncode(diff); err != nil { - resp.Diagnostics = convert.AppendProtoDiag(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(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(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(_ context.Context, req *proto.ApplyResourceChange_Request) (*proto.ApplyResourceChange_Response, error) { - resp := &proto.ApplyResourceChange_Response{ - // Start with the existing state as a fallback - NewState: req.PriorState, - } - - res := s.provider.ResourcesMap[req.TypeName] - schemaBlock := s.getResourceSchemaBlock(req.TypeName) - - priorStateVal, err := msgpack.Unmarshal(req.PriorState.Msgpack, schemaBlock.ImpliedType()) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - return resp, nil - } - - plannedStateVal, err := msgpack.Unmarshal(req.PlannedState.Msgpack, schemaBlock.ImpliedType()) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - return resp, nil - } - - info := &terraform.InstanceInfo{ - Type: req.TypeName, - } - - priorState, err := res.ShimInstanceStateFromValue(priorStateVal) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(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(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, - } - } else { - diff, err = schema.DiffFromValues(priorStateVal, plannedStateVal, stripResourceModifiers(res)) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - return resp, nil - } - } - - if diff == nil { - diff = &terraform.InstanceDiff{ - Attributes: make(map[string]*terraform.ResourceAttrDiff), - Meta: make(map[string]interface{}), - } - } - - // 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(resp.Diagnostics, err) - return resp, nil - } - priorState.ProviderMeta = providerSchemaVal - } - - newInstanceState, err := s.provider.Apply(info, priorState, diff) - // we record the error here, but continue processing any returned state. - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - } - 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(resp.Diagnostics, err) - return resp, nil - } - resp.NewState = &proto.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 = schema.StateValueFromInstanceState(newInstanceState, schemaBlock.ImpliedType()) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(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(resp.Diagnostics, err) - return resp, nil - } - resp.NewState = &proto.DynamicValue{ - Msgpack: newStateMP, - } - - meta, err := json.Marshal(newInstanceState.Meta) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(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.LegacyTypeSystem = true - - return resp, nil -} - -func (s *GRPCProviderServer) ImportResourceState(_ context.Context, req *proto.ImportResourceState_Request) (*proto.ImportResourceState_Response, error) { - resp := &proto.ImportResourceState_Response{} - - info := &terraform.InstanceInfo{ - Type: req.TypeName, - } - - newInstanceStates, err := s.provider.ImportState(info, req.Id) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(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(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(resp.Diagnostics, err) - return resp, nil - } - - meta, err := json.Marshal(is.Meta) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - return resp, nil - } - - importedResource := &proto.ImportResourceState_ImportedResource{ - TypeName: resourceType, - State: &proto.DynamicValue{ - Msgpack: newStateMP, - }, - Private: meta, - } - - resp.ImportedResources = append(resp.ImportedResources, importedResource) - } - - return resp, nil -} - -func (s *GRPCProviderServer) ReadDataSource(_ context.Context, req *proto.ReadDataSource_Request) (*proto.ReadDataSource_Response, error) { - resp := &proto.ReadDataSource_Response{} - - schemaBlock := s.getDatasourceSchemaBlock(req.TypeName) - - configVal, err := msgpack.Unmarshal(req.Config.Msgpack, schemaBlock.ImpliedType()) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - return resp, nil - } - - info := &terraform.InstanceInfo{ - Type: req.TypeName, - } - - // Ensure there are no nulls that will cause helper/schema to panic. - if err := validateConfigNulls(configVal, nil); err != nil { - resp.Diagnostics = convert.AppendProtoDiag(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 - diff, err := s.provider.ReadDataDiff(info, config) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - return resp, nil - } - - // now we can get the new complete data source - newInstanceState, err := s.provider.ReadDataApply(info, diff) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - return resp, nil - } - - newStateVal, err := schema.StateValueFromInstanceState(newInstanceState, schemaBlock.ImpliedType()) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - return resp, nil - } - - newStateVal = copyTimeoutValues(newStateVal, configVal) - - newStateMP, err := msgpack.Marshal(newStateVal, schemaBlock.ImpliedType()) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - return resp, nil - } - resp.State = &proto.DynamicValue{ - Msgpack: newStateMP, - } - return resp, nil -} - -func pathToAttributePath(path cty.Path) *proto.AttributePath { - var steps []*proto.AttributePath_Step - - for _, step := range path { - switch s := step.(type) { - case cty.GetAttrStep: - steps = append(steps, &proto.AttributePath_Step{ - Selector: &proto.AttributePath_Step_AttributeName{ - AttributeName: s.Name, - }, - }) - case cty.IndexStep: - ty := s.Key.Type() - switch ty { - case cty.Number: - i, _ := s.Key.AsBigFloat().Int64() - steps = append(steps, &proto.AttributePath_Step{ - Selector: &proto.AttributePath_Step_ElementKeyInt{ - ElementKeyInt: i, - }, - }) - case cty.String: - steps = append(steps, &proto.AttributePath_Step{ - Selector: &proto.AttributePath_Step_ElementKeyString{ - ElementKeyString: s.Key.AsString(), - }, - }) - } - } - } - - return &proto.AttributePath{Steps: 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[schema.TimeoutsConfigKey] - if ok { - toAttrs[schema.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[schema.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[schema.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 *schema.Resource) *schema.Resource { - if r == nil { - return nil - } - // start with a shallow copy - newResource := new(schema.Resource) - *newResource = *r - - newResource.CustomizeDiff = nil - newResource.Schema = map[string]*schema.Schema{} - - for k, s := range r.Schema { - newResource.Schema[k] = stripSchema(s) - } - - return newResource -} - -func stripSchema(s *schema.Schema) *schema.Schema { - if s == nil { - return nil - } - // start with a shallow copy - newSchema := new(schema.Schema) - *newSchema = *s - - newSchema.StateFunc = nil - - switch e := newSchema.Elem.(type) { - case *schema.Schema: - newSchema.Elem = stripSchema(e) - case *schema.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(v cty.Value, path cty.Path) []*proto.Diagnostic { - var diags []*proto.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, &proto.Diagnostic{ - Severity: proto.Diagnostic_ERROR, - Summary: "Null value found in list", - Detail: "Null values are not allowed for this attribute value.", - Attribute: convert.PathToAttributePath(p), - }) - continue - } - - d := validateConfigNulls(ev, append(path, cty.IndexStep{Key: kv})) - diags = convert.AppendProtoDiag(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(ev, append(path, step)) - diags = convert.AppendProtoDiag(diags, d) - } - } - - return diags -} diff --git a/helper/plugin/grpc_provider_test.go b/helper/plugin/grpc_provider_test.go deleted file mode 100644 index 736eb258e..000000000 --- a/helper/plugin/grpc_provider_test.go +++ /dev/null @@ -1,1382 +0,0 @@ -package plugin - -import ( - "context" - "fmt" - "strconv" - "strings" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/hashicorp/terraform/helper/schema" - proto "github.com/hashicorp/terraform/internal/tfplugin5" - "github.com/hashicorp/terraform/plugin/convert" - "github.com/hashicorp/terraform/terraform" - "github.com/zclconf/go-cty/cty" - "github.com/zclconf/go-cty/cty/msgpack" -) - -// The GRPCProviderServer will directly implement the go protobuf server -var _ proto.ProviderServer = (*GRPCProviderServer)(nil) - -var ( - typeComparer = cmp.Comparer(cty.Type.Equals) - valueComparer = cmp.Comparer(cty.Value.RawEquals) - equateEmpty = cmpopts.EquateEmpty() -) - -func TestUpgradeState_jsonState(t *testing.T) { - r := &schema.Resource{ - SchemaVersion: 2, - Schema: map[string]*schema.Schema{ - "two": { - Type: schema.TypeInt, - Optional: true, - }, - }, - } - - r.StateUpgraders = []schema.StateUpgrader{ - { - Version: 0, - Type: cty.Object(map[string]cty.Type{ - "id": cty.String, - "zero": cty.Number, - }), - Upgrade: func(m map[string]interface{}, meta interface{}) (map[string]interface{}, error) { - _, ok := m["zero"].(float64) - if !ok { - return nil, fmt.Errorf("zero not found in %#v", m) - } - m["one"] = float64(1) - delete(m, "zero") - return m, nil - }, - }, - { - Version: 1, - Type: cty.Object(map[string]cty.Type{ - "id": cty.String, - "one": cty.Number, - }), - Upgrade: func(m map[string]interface{}, meta interface{}) (map[string]interface{}, error) { - _, ok := m["one"].(float64) - if !ok { - return nil, fmt.Errorf("one not found in %#v", m) - } - m["two"] = float64(2) - delete(m, "one") - return m, nil - }, - }, - } - - server := &GRPCProviderServer{ - provider: &schema.Provider{ - ResourcesMap: map[string]*schema.Resource{ - "test": r, - }, - }, - } - - req := &proto.UpgradeResourceState_Request{ - TypeName: "test", - Version: 0, - RawState: &proto.RawState{ - Json: []byte(`{"id":"bar","zero":0}`), - }, - } - - resp, err := server.UpgradeResourceState(nil, req) - if err != nil { - t.Fatal(err) - } - - if len(resp.Diagnostics) > 0 { - for _, d := range resp.Diagnostics { - t.Errorf("%#v", d) - } - t.Fatal("error") - } - - val, err := msgpack.Unmarshal(resp.UpgradedState.Msgpack, r.CoreConfigSchema().ImpliedType()) - if err != nil { - t.Fatal(err) - } - - expected := cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("bar"), - "two": cty.NumberIntVal(2), - }) - - if !cmp.Equal(expected, val, valueComparer, equateEmpty) { - t.Fatal(cmp.Diff(expected, val, valueComparer, equateEmpty)) - } -} - -func TestUpgradeState_removedAttr(t *testing.T) { - r1 := &schema.Resource{ - Schema: map[string]*schema.Schema{ - "two": { - Type: schema.TypeString, - Optional: true, - }, - }, - } - - r2 := &schema.Resource{ - Schema: map[string]*schema.Schema{ - "multi": { - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "set": { - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "required": { - Type: schema.TypeString, - Required: true, - }, - }, - }, - }, - }, - }, - }, - }, - } - - r3 := &schema.Resource{ - Schema: map[string]*schema.Schema{ - "config_mode_attr": { - Type: schema.TypeList, - ConfigMode: schema.SchemaConfigModeAttr, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "foo": { - Type: schema.TypeString, - Optional: true, - }, - }, - }, - }, - }, - } - - p := &schema.Provider{ - ResourcesMap: map[string]*schema.Resource{ - "r1": r1, - "r2": r2, - "r3": r3, - }, - } - - server := &GRPCProviderServer{ - provider: p, - } - - for _, tc := range []struct { - name string - raw string - expected cty.Value - }{ - { - name: "r1", - raw: `{"id":"bar","removed":"removed","two":"2"}`, - expected: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("bar"), - "two": cty.StringVal("2"), - }), - }, - { - name: "r2", - raw: `{"id":"bar","multi":[{"set":[{"required":"ok","removed":"removed"}]}]}`, - expected: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("bar"), - "multi": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "set": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "required": cty.StringVal("ok"), - }), - }), - }), - }), - }), - }, - { - name: "r3", - raw: `{"id":"bar","config_mode_attr":[{"foo":"ok","removed":"removed"}]}`, - expected: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("bar"), - "config_mode_attr": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "foo": cty.StringVal("ok"), - }), - }), - }), - }, - } { - t.Run(tc.name, func(t *testing.T) { - req := &proto.UpgradeResourceState_Request{ - TypeName: tc.name, - Version: 0, - RawState: &proto.RawState{ - Json: []byte(tc.raw), - }, - } - resp, err := server.UpgradeResourceState(nil, req) - if err != nil { - t.Fatal(err) - } - - if len(resp.Diagnostics) > 0 { - for _, d := range resp.Diagnostics { - t.Errorf("%#v", d) - } - t.Fatal("error") - } - val, err := msgpack.Unmarshal(resp.UpgradedState.Msgpack, p.ResourcesMap[tc.name].CoreConfigSchema().ImpliedType()) - if err != nil { - t.Fatal(err) - } - if !tc.expected.RawEquals(val) { - t.Fatalf("\nexpected: %#v\ngot: %#v\n", tc.expected, val) - } - }) - } - -} - -func TestUpgradeState_flatmapState(t *testing.T) { - r := &schema.Resource{ - SchemaVersion: 4, - Schema: map[string]*schema.Schema{ - "four": { - Type: schema.TypeInt, - Required: true, - }, - "block": { - Type: schema.TypeList, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "attr": { - Type: schema.TypeString, - Optional: true, - }, - }, - }, - }, - }, - // this MigrateState will take the state to version 2 - MigrateState: func(v int, is *terraform.InstanceState, _ interface{}) (*terraform.InstanceState, error) { - switch v { - case 0: - _, ok := is.Attributes["zero"] - if !ok { - return nil, fmt.Errorf("zero not found in %#v", is.Attributes) - } - is.Attributes["one"] = "1" - delete(is.Attributes, "zero") - fallthrough - case 1: - _, ok := is.Attributes["one"] - if !ok { - return nil, fmt.Errorf("one not found in %#v", is.Attributes) - } - is.Attributes["two"] = "2" - delete(is.Attributes, "one") - default: - return nil, fmt.Errorf("invalid schema version %d", v) - } - return is, nil - }, - } - - r.StateUpgraders = []schema.StateUpgrader{ - { - Version: 2, - Type: cty.Object(map[string]cty.Type{ - "id": cty.String, - "two": cty.Number, - }), - Upgrade: func(m map[string]interface{}, meta interface{}) (map[string]interface{}, error) { - _, ok := m["two"].(float64) - if !ok { - return nil, fmt.Errorf("two not found in %#v", m) - } - m["three"] = float64(3) - delete(m, "two") - return m, nil - }, - }, - { - Version: 3, - Type: cty.Object(map[string]cty.Type{ - "id": cty.String, - "three": cty.Number, - }), - Upgrade: func(m map[string]interface{}, meta interface{}) (map[string]interface{}, error) { - _, ok := m["three"].(float64) - if !ok { - return nil, fmt.Errorf("three not found in %#v", m) - } - m["four"] = float64(4) - delete(m, "three") - return m, nil - }, - }, - } - - server := &GRPCProviderServer{ - provider: &schema.Provider{ - ResourcesMap: map[string]*schema.Resource{ - "test": r, - }, - }, - } - - testReqs := []*proto.UpgradeResourceState_Request{ - { - TypeName: "test", - Version: 0, - RawState: &proto.RawState{ - Flatmap: map[string]string{ - "id": "bar", - "zero": "0", - }, - }, - }, - { - TypeName: "test", - Version: 1, - RawState: &proto.RawState{ - Flatmap: map[string]string{ - "id": "bar", - "one": "1", - }, - }, - }, - // two and up could be stored in flatmap or json states - { - TypeName: "test", - Version: 2, - RawState: &proto.RawState{ - Flatmap: map[string]string{ - "id": "bar", - "two": "2", - }, - }, - }, - { - TypeName: "test", - Version: 2, - RawState: &proto.RawState{ - Json: []byte(`{"id":"bar","two":2}`), - }, - }, - { - TypeName: "test", - Version: 3, - RawState: &proto.RawState{ - Flatmap: map[string]string{ - "id": "bar", - "three": "3", - }, - }, - }, - { - TypeName: "test", - Version: 3, - RawState: &proto.RawState{ - Json: []byte(`{"id":"bar","three":3}`), - }, - }, - { - TypeName: "test", - Version: 4, - RawState: &proto.RawState{ - Flatmap: map[string]string{ - "id": "bar", - "four": "4", - }, - }, - }, - { - TypeName: "test", - Version: 4, - RawState: &proto.RawState{ - Json: []byte(`{"id":"bar","four":4}`), - }, - }, - } - - for i, req := range testReqs { - t.Run(fmt.Sprintf("%d-%d", i, req.Version), func(t *testing.T) { - resp, err := server.UpgradeResourceState(nil, req) - if err != nil { - t.Fatal(err) - } - - if len(resp.Diagnostics) > 0 { - for _, d := range resp.Diagnostics { - t.Errorf("%#v", d) - } - t.Fatal("error") - } - - val, err := msgpack.Unmarshal(resp.UpgradedState.Msgpack, r.CoreConfigSchema().ImpliedType()) - if err != nil { - t.Fatal(err) - } - - expected := cty.ObjectVal(map[string]cty.Value{ - "block": cty.ListValEmpty(cty.Object(map[string]cty.Type{"attr": cty.String})), - "id": cty.StringVal("bar"), - "four": cty.NumberIntVal(4), - }) - - if !cmp.Equal(expected, val, valueComparer, equateEmpty) { - t.Fatal(cmp.Diff(expected, val, valueComparer, equateEmpty)) - } - }) - } -} - -func TestUpgradeState_flatmapStateMissingMigrateState(t *testing.T) { - r := &schema.Resource{ - SchemaVersion: 1, - Schema: map[string]*schema.Schema{ - "one": { - Type: schema.TypeInt, - Required: true, - }, - }, - } - - server := &GRPCProviderServer{ - provider: &schema.Provider{ - ResourcesMap: map[string]*schema.Resource{ - "test": r, - }, - }, - } - - testReqs := []*proto.UpgradeResourceState_Request{ - { - TypeName: "test", - Version: 0, - RawState: &proto.RawState{ - Flatmap: map[string]string{ - "id": "bar", - "one": "1", - }, - }, - }, - { - TypeName: "test", - Version: 1, - RawState: &proto.RawState{ - Flatmap: map[string]string{ - "id": "bar", - "one": "1", - }, - }, - }, - { - TypeName: "test", - Version: 1, - RawState: &proto.RawState{ - Json: []byte(`{"id":"bar","one":1}`), - }, - }, - } - - for i, req := range testReqs { - t.Run(fmt.Sprintf("%d-%d", i, req.Version), func(t *testing.T) { - resp, err := server.UpgradeResourceState(nil, req) - if err != nil { - t.Fatal(err) - } - - if len(resp.Diagnostics) > 0 { - for _, d := range resp.Diagnostics { - t.Errorf("%#v", d) - } - t.Fatal("error") - } - - val, err := msgpack.Unmarshal(resp.UpgradedState.Msgpack, r.CoreConfigSchema().ImpliedType()) - if err != nil { - t.Fatal(err) - } - - expected := cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("bar"), - "one": cty.NumberIntVal(1), - }) - - if !cmp.Equal(expected, val, valueComparer, equateEmpty) { - t.Fatal(cmp.Diff(expected, val, valueComparer, equateEmpty)) - } - }) - } -} - -func TestPlanResourceChange(t *testing.T) { - r := &schema.Resource{ - SchemaVersion: 4, - Schema: map[string]*schema.Schema{ - "foo": { - Type: schema.TypeInt, - Optional: true, - }, - }, - } - - server := &GRPCProviderServer{ - provider: &schema.Provider{ - ResourcesMap: map[string]*schema.Resource{ - "test": r, - }, - }, - } - - schema := r.CoreConfigSchema() - priorState, err := msgpack.Marshal(cty.NullVal(schema.ImpliedType()), schema.ImpliedType()) - if err != nil { - t.Fatal(err) - } - - // A propsed state with only the ID unknown will produce a nil diff, and - // should return the propsed state value. - proposedVal, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - })) - if err != nil { - t.Fatal(err) - } - proposedState, err := msgpack.Marshal(proposedVal, schema.ImpliedType()) - if err != nil { - t.Fatal(err) - } - - testReq := &proto.PlanResourceChange_Request{ - TypeName: "test", - PriorState: &proto.DynamicValue{ - Msgpack: priorState, - }, - ProposedNewState: &proto.DynamicValue{ - Msgpack: proposedState, - }, - } - - resp, err := server.PlanResourceChange(context.Background(), testReq) - if err != nil { - t.Fatal(err) - } - - plannedStateVal, err := msgpack.Unmarshal(resp.PlannedState.Msgpack, schema.ImpliedType()) - if err != nil { - t.Fatal(err) - } - - if !cmp.Equal(proposedVal, plannedStateVal, valueComparer) { - t.Fatal(cmp.Diff(proposedVal, plannedStateVal, valueComparer)) - } -} - -func TestApplyResourceChange(t *testing.T) { - r := &schema.Resource{ - SchemaVersion: 4, - Schema: map[string]*schema.Schema{ - "foo": { - Type: schema.TypeInt, - Optional: true, - }, - }, - Create: func(rd *schema.ResourceData, _ interface{}) error { - rd.SetId("bar") - return nil - }, - } - - server := &GRPCProviderServer{ - provider: &schema.Provider{ - ResourcesMap: map[string]*schema.Resource{ - "test": r, - }, - }, - } - - schema := r.CoreConfigSchema() - priorState, err := msgpack.Marshal(cty.NullVal(schema.ImpliedType()), schema.ImpliedType()) - if err != nil { - t.Fatal(err) - } - - // A proposed state with only the ID unknown will produce a nil diff, and - // should return the proposed state value. - plannedVal, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - })) - if err != nil { - t.Fatal(err) - } - plannedState, err := msgpack.Marshal(plannedVal, schema.ImpliedType()) - if err != nil { - t.Fatal(err) - } - - testReq := &proto.ApplyResourceChange_Request{ - TypeName: "test", - PriorState: &proto.DynamicValue{ - Msgpack: priorState, - }, - PlannedState: &proto.DynamicValue{ - Msgpack: plannedState, - }, - } - - resp, err := server.ApplyResourceChange(context.Background(), testReq) - if err != nil { - t.Fatal(err) - } - - newStateVal, err := msgpack.Unmarshal(resp.NewState.Msgpack, schema.ImpliedType()) - if err != nil { - t.Fatal(err) - } - - id := newStateVal.GetAttr("id").AsString() - if id != "bar" { - t.Fatalf("incorrect final state: %#v\n", newStateVal) - } -} - -func TestPrepareProviderConfig(t *testing.T) { - for _, tc := range []struct { - Name string - Schema map[string]*schema.Schema - ConfigVal cty.Value - ExpectError string - ExpectConfig cty.Value - }{ - { - Name: "test prepare", - Schema: map[string]*schema.Schema{ - "foo": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - }, - ConfigVal: cty.ObjectVal(map[string]cty.Value{ - "foo": cty.StringVal("bar"), - }), - ExpectConfig: cty.ObjectVal(map[string]cty.Value{ - "foo": cty.StringVal("bar"), - }), - }, - { - Name: "test default", - Schema: map[string]*schema.Schema{ - "foo": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Default: "default", - }, - }, - ConfigVal: cty.ObjectVal(map[string]cty.Value{ - "foo": cty.NullVal(cty.String), - }), - ExpectConfig: cty.ObjectVal(map[string]cty.Value{ - "foo": cty.StringVal("default"), - }), - }, - { - Name: "test defaultfunc", - Schema: map[string]*schema.Schema{ - "foo": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - DefaultFunc: func() (interface{}, error) { - return "defaultfunc", nil - }, - }, - }, - ConfigVal: cty.ObjectVal(map[string]cty.Value{ - "foo": cty.NullVal(cty.String), - }), - ExpectConfig: cty.ObjectVal(map[string]cty.Value{ - "foo": cty.StringVal("defaultfunc"), - }), - }, - { - Name: "test default required", - Schema: map[string]*schema.Schema{ - "foo": &schema.Schema{ - Type: schema.TypeString, - Required: true, - DefaultFunc: func() (interface{}, error) { - return "defaultfunc", nil - }, - }, - }, - ConfigVal: cty.ObjectVal(map[string]cty.Value{ - "foo": cty.NullVal(cty.String), - }), - ExpectConfig: cty.ObjectVal(map[string]cty.Value{ - "foo": cty.StringVal("defaultfunc"), - }), - }, - { - Name: "test incorrect type", - Schema: map[string]*schema.Schema{ - "foo": &schema.Schema{ - Type: schema.TypeString, - Required: true, - }, - }, - ConfigVal: cty.ObjectVal(map[string]cty.Value{ - "foo": cty.NumberIntVal(3), - }), - ExpectConfig: cty.ObjectVal(map[string]cty.Value{ - "foo": cty.StringVal("3"), - }), - }, - { - Name: "test incorrect default type", - Schema: map[string]*schema.Schema{ - "foo": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Default: true, - }, - }, - ConfigVal: cty.ObjectVal(map[string]cty.Value{ - "foo": cty.NullVal(cty.String), - }), - ExpectConfig: cty.ObjectVal(map[string]cty.Value{ - "foo": cty.StringVal("true"), - }), - }, - { - Name: "test incorrect default bool type", - Schema: map[string]*schema.Schema{ - "foo": &schema.Schema{ - Type: schema.TypeBool, - Optional: true, - Default: "", - }, - }, - ConfigVal: cty.ObjectVal(map[string]cty.Value{ - "foo": cty.NullVal(cty.Bool), - }), - ExpectConfig: cty.ObjectVal(map[string]cty.Value{ - "foo": cty.False, - }), - }, - { - Name: "test deprecated default", - Schema: map[string]*schema.Schema{ - "foo": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Default: "do not use", - Removed: "don't use this", - }, - }, - ConfigVal: cty.ObjectVal(map[string]cty.Value{ - "foo": cty.NullVal(cty.String), - }), - ExpectConfig: cty.ObjectVal(map[string]cty.Value{ - "foo": cty.NullVal(cty.String), - }), - }, - } { - t.Run(tc.Name, func(t *testing.T) { - server := &GRPCProviderServer{ - provider: &schema.Provider{ - Schema: tc.Schema, - }, - } - - block := schema.InternalMap(tc.Schema).CoreConfigSchema() - - rawConfig, err := msgpack.Marshal(tc.ConfigVal, block.ImpliedType()) - if err != nil { - t.Fatal(err) - } - - testReq := &proto.PrepareProviderConfig_Request{ - Config: &proto.DynamicValue{ - Msgpack: rawConfig, - }, - } - - resp, err := server.PrepareProviderConfig(nil, testReq) - if err != nil { - t.Fatal(err) - } - - if tc.ExpectError != "" && len(resp.Diagnostics) > 0 { - for _, d := range resp.Diagnostics { - if !strings.Contains(d.Summary, tc.ExpectError) { - t.Fatalf("Unexpected error: %s/%s", d.Summary, d.Detail) - } - } - return - } - - // we should have no errors past this point - for _, d := range resp.Diagnostics { - if d.Severity == proto.Diagnostic_ERROR { - t.Fatal(resp.Diagnostics) - } - } - - val, err := msgpack.Unmarshal(resp.PreparedConfig.Msgpack, block.ImpliedType()) - if err != nil { - t.Fatal(err) - } - - if tc.ExpectConfig.GoString() != val.GoString() { - t.Fatalf("\nexpected: %#v\ngot: %#v", tc.ExpectConfig, val) - } - }) - } -} - -func TestGetSchemaTimeouts(t *testing.T) { - r := &schema.Resource{ - SchemaVersion: 4, - Timeouts: &schema.ResourceTimeout{ - Create: schema.DefaultTimeout(time.Second), - Read: schema.DefaultTimeout(2 * time.Second), - Update: schema.DefaultTimeout(3 * time.Second), - Default: schema.DefaultTimeout(10 * time.Second), - }, - Schema: map[string]*schema.Schema{ - "foo": { - Type: schema.TypeInt, - Optional: true, - }, - }, - } - - // verify that the timeouts appear in the schema as defined - block := r.CoreConfigSchema() - timeoutsBlock := block.BlockTypes["timeouts"] - if timeoutsBlock == nil { - t.Fatal("missing timeouts in schema") - } - - if timeoutsBlock.Attributes["create"] == nil { - t.Fatal("missing create timeout in schema") - } - if timeoutsBlock.Attributes["read"] == nil { - t.Fatal("missing read timeout in schema") - } - if timeoutsBlock.Attributes["update"] == nil { - t.Fatal("missing update timeout in schema") - } - if d := timeoutsBlock.Attributes["delete"]; d != nil { - t.Fatalf("unexpected delete timeout in schema: %#v", d) - } - if timeoutsBlock.Attributes["default"] == nil { - t.Fatal("missing default timeout in schema") - } -} - -func TestNormalizeNullValues(t *testing.T) { - for i, tc := range []struct { - Src, Dst, Expect cty.Value - Apply bool - }{ - { - // The known set value is copied over the null set value - Src: cty.ObjectVal(map[string]cty.Value{ - "set": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "foo": cty.NullVal(cty.String), - }), - }), - }), - Dst: cty.ObjectVal(map[string]cty.Value{ - "set": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ - "foo": cty.String, - }))), - }), - Expect: cty.ObjectVal(map[string]cty.Value{ - "set": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "foo": cty.NullVal(cty.String), - }), - }), - }), - Apply: true, - }, - { - // A zero set value is kept - Src: cty.ObjectVal(map[string]cty.Value{ - "set": cty.SetValEmpty(cty.String), - }), - Dst: cty.ObjectVal(map[string]cty.Value{ - "set": cty.SetValEmpty(cty.String), - }), - Expect: cty.ObjectVal(map[string]cty.Value{ - "set": cty.SetValEmpty(cty.String), - }), - }, - { - // The known set value is copied over the null set value - Src: cty.ObjectVal(map[string]cty.Value{ - "set": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "foo": cty.NullVal(cty.String), - }), - }), - }), - Dst: cty.ObjectVal(map[string]cty.Value{ - "set": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ - "foo": cty.String, - }))), - }), - // If we're only in a plan, we can't compare sets at all - Expect: cty.ObjectVal(map[string]cty.Value{ - "set": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ - "foo": cty.String, - }))), - }), - }, - { - // The empty map is copied over the null map - Src: cty.ObjectVal(map[string]cty.Value{ - "map": cty.MapValEmpty(cty.String), - }), - Dst: cty.ObjectVal(map[string]cty.Value{ - "map": cty.NullVal(cty.Map(cty.String)), - }), - Expect: cty.ObjectVal(map[string]cty.Value{ - "map": cty.MapValEmpty(cty.String), - }), - Apply: true, - }, - { - // A zero value primitive is copied over a null primitive - Src: cty.ObjectVal(map[string]cty.Value{ - "string": cty.StringVal(""), - }), - Dst: cty.ObjectVal(map[string]cty.Value{ - "string": cty.NullVal(cty.String), - }), - Expect: cty.ObjectVal(map[string]cty.Value{ - "string": cty.StringVal(""), - }), - Apply: true, - }, - { - // Plan primitives are kept - Src: cty.ObjectVal(map[string]cty.Value{ - "string": cty.NumberIntVal(0), - }), - Dst: cty.ObjectVal(map[string]cty.Value{ - "string": cty.NullVal(cty.Number), - }), - Expect: cty.ObjectVal(map[string]cty.Value{ - "string": cty.NullVal(cty.Number), - }), - }, - { - // Neither plan nor apply should remove empty strings - Src: cty.ObjectVal(map[string]cty.Value{ - "string": cty.StringVal(""), - }), - Dst: cty.ObjectVal(map[string]cty.Value{ - "string": cty.NullVal(cty.String), - }), - Expect: cty.ObjectVal(map[string]cty.Value{ - "string": cty.StringVal(""), - }), - }, - { - // Neither plan nor apply should remove empty strings - Src: cty.ObjectVal(map[string]cty.Value{ - "string": cty.StringVal(""), - }), - Dst: cty.ObjectVal(map[string]cty.Value{ - "string": cty.NullVal(cty.String), - }), - Expect: cty.ObjectVal(map[string]cty.Value{ - "string": cty.StringVal(""), - }), - Apply: true, - }, - { - // The null map is retained, because the src was unknown - Src: cty.ObjectVal(map[string]cty.Value{ - "map": cty.UnknownVal(cty.Map(cty.String)), - }), - Dst: cty.ObjectVal(map[string]cty.Value{ - "map": cty.NullVal(cty.Map(cty.String)), - }), - Expect: cty.ObjectVal(map[string]cty.Value{ - "map": cty.NullVal(cty.Map(cty.String)), - }), - Apply: true, - }, - { - // the nul set is retained, because the src set contains an unknown value - Src: cty.ObjectVal(map[string]cty.Value{ - "set": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "foo": cty.UnknownVal(cty.String), - }), - }), - }), - Dst: cty.ObjectVal(map[string]cty.Value{ - "set": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ - "foo": cty.String, - }))), - }), - Expect: cty.ObjectVal(map[string]cty.Value{ - "set": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ - "foo": cty.String, - }))), - }), - Apply: true, - }, - { - // Retain don't re-add unexpected planned values in a map - Src: cty.ObjectVal(map[string]cty.Value{ - "map": cty.MapVal(map[string]cty.Value{ - "a": cty.StringVal("a"), - "b": cty.StringVal(""), - }), - }), - Dst: cty.ObjectVal(map[string]cty.Value{ - "map": cty.MapVal(map[string]cty.Value{ - "a": cty.StringVal("a"), - }), - }), - Expect: cty.ObjectVal(map[string]cty.Value{ - "map": cty.MapVal(map[string]cty.Value{ - "a": cty.StringVal("a"), - }), - }), - }, - { - // Remove extra values after apply - Src: cty.ObjectVal(map[string]cty.Value{ - "map": cty.MapVal(map[string]cty.Value{ - "a": cty.StringVal("a"), - "b": cty.StringVal("b"), - }), - }), - Dst: cty.ObjectVal(map[string]cty.Value{ - "map": cty.MapVal(map[string]cty.Value{ - "a": cty.StringVal("a"), - }), - }), - Expect: cty.ObjectVal(map[string]cty.Value{ - "map": cty.MapVal(map[string]cty.Value{ - "a": cty.StringVal("a"), - }), - }), - Apply: true, - }, - { - Src: cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("a"), - }), - Dst: cty.EmptyObjectVal, - Expect: cty.ObjectVal(map[string]cty.Value{ - "a": cty.NullVal(cty.String), - }), - }, - - // a list in an object in a list, going from null to empty - { - Src: cty.ObjectVal(map[string]cty.Value{ - "network_interface": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "network_ip": cty.UnknownVal(cty.String), - "access_config": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{"public_ptr_domain_name": cty.String, "nat_ip": cty.String}))), - "address": cty.NullVal(cty.String), - "name": cty.StringVal("nic0"), - })}), - }), - Dst: cty.ObjectVal(map[string]cty.Value{ - "network_interface": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "network_ip": cty.StringVal("10.128.0.64"), - "access_config": cty.ListValEmpty(cty.Object(map[string]cty.Type{"public_ptr_domain_name": cty.String, "nat_ip": cty.String})), - "address": cty.StringVal("address"), - "name": cty.StringVal("nic0"), - }), - }), - }), - Expect: cty.ObjectVal(map[string]cty.Value{ - "network_interface": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "network_ip": cty.StringVal("10.128.0.64"), - "access_config": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{"public_ptr_domain_name": cty.String, "nat_ip": cty.String}))), - "address": cty.StringVal("address"), - "name": cty.StringVal("nic0"), - }), - }), - }), - Apply: true, - }, - - // a list in an object in a list, going from empty to null - { - Src: cty.ObjectVal(map[string]cty.Value{ - "network_interface": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "network_ip": cty.UnknownVal(cty.String), - "access_config": cty.ListValEmpty(cty.Object(map[string]cty.Type{"public_ptr_domain_name": cty.String, "nat_ip": cty.String})), - "address": cty.NullVal(cty.String), - "name": cty.StringVal("nic0"), - })}), - }), - Dst: cty.ObjectVal(map[string]cty.Value{ - "network_interface": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "network_ip": cty.StringVal("10.128.0.64"), - "access_config": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{"public_ptr_domain_name": cty.String, "nat_ip": cty.String}))), - "address": cty.StringVal("address"), - "name": cty.StringVal("nic0"), - }), - }), - }), - Expect: cty.ObjectVal(map[string]cty.Value{ - "network_interface": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "network_ip": cty.StringVal("10.128.0.64"), - "access_config": cty.ListValEmpty(cty.Object(map[string]cty.Type{"public_ptr_domain_name": cty.String, "nat_ip": cty.String})), - "address": cty.StringVal("address"), - "name": cty.StringVal("nic0"), - }), - }), - }), - Apply: true, - }, - // the empty list should be transferred, but the new unknown should not be overridden - { - Src: cty.ObjectVal(map[string]cty.Value{ - "network_interface": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "network_ip": cty.StringVal("10.128.0.64"), - "access_config": cty.ListValEmpty(cty.Object(map[string]cty.Type{"public_ptr_domain_name": cty.String, "nat_ip": cty.String})), - "address": cty.NullVal(cty.String), - "name": cty.StringVal("nic0"), - })}), - }), - Dst: cty.ObjectVal(map[string]cty.Value{ - "network_interface": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "network_ip": cty.UnknownVal(cty.String), - "access_config": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{"public_ptr_domain_name": cty.String, "nat_ip": cty.String}))), - "address": cty.StringVal("address"), - "name": cty.StringVal("nic0"), - }), - }), - }), - Expect: cty.ObjectVal(map[string]cty.Value{ - "network_interface": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "network_ip": cty.UnknownVal(cty.String), - "access_config": cty.ListValEmpty(cty.Object(map[string]cty.Type{"public_ptr_domain_name": cty.String, "nat_ip": cty.String})), - "address": cty.StringVal("address"), - "name": cty.StringVal("nic0"), - }), - }), - }), - }, - { - // fix unknowns added to a map - Src: cty.ObjectVal(map[string]cty.Value{ - "map": cty.MapVal(map[string]cty.Value{ - "a": cty.StringVal("a"), - "b": cty.StringVal(""), - }), - }), - Dst: cty.ObjectVal(map[string]cty.Value{ - "map": cty.MapVal(map[string]cty.Value{ - "a": cty.StringVal("a"), - "b": cty.UnknownVal(cty.String), - }), - }), - Expect: cty.ObjectVal(map[string]cty.Value{ - "map": cty.MapVal(map[string]cty.Value{ - "a": cty.StringVal("a"), - "b": cty.StringVal(""), - }), - }), - }, - { - // fix unknowns lost from a list - Src: cty.ObjectVal(map[string]cty.Value{ - "top": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "list": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "values": cty.ListVal([]cty.Value{cty.UnknownVal(cty.String)}), - }), - }), - }), - }), - }), - Dst: cty.ObjectVal(map[string]cty.Value{ - "top": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "list": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "values": cty.NullVal(cty.List(cty.String)), - }), - }), - }), - }), - }), - Expect: cty.ObjectVal(map[string]cty.Value{ - "top": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "list": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "values": cty.ListVal([]cty.Value{cty.UnknownVal(cty.String)}), - }), - }), - }), - }), - }), - }, - { - Src: cty.ObjectVal(map[string]cty.Value{ - "set": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ - "list": cty.List(cty.String), - }))), - }), - Dst: cty.ObjectVal(map[string]cty.Value{ - "set": cty.SetValEmpty(cty.Object(map[string]cty.Type{ - "list": cty.List(cty.String), - })), - }), - Expect: cty.ObjectVal(map[string]cty.Value{ - "set": cty.SetValEmpty(cty.Object(map[string]cty.Type{ - "list": cty.List(cty.String), - })), - }), - }, - { - Src: cty.ObjectVal(map[string]cty.Value{ - "set": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ - "list": cty.List(cty.String), - }))), - }), - Dst: cty.ObjectVal(map[string]cty.Value{ - "set": cty.SetValEmpty(cty.Object(map[string]cty.Type{ - "list": cty.List(cty.String), - })), - }), - Expect: cty.ObjectVal(map[string]cty.Value{ - "set": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ - "list": cty.List(cty.String), - }))), - }), - Apply: true, - }, - } { - t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { - got := normalizeNullValues(tc.Dst, tc.Src, tc.Apply) - if !got.RawEquals(tc.Expect) { - t.Fatalf("\nexpected: %#v\ngot: %#v\n", tc.Expect, got) - } - }) - } -} - -func TestValidateNulls(t *testing.T) { - for i, tc := range []struct { - Cfg cty.Value - Err bool - }{ - { - Cfg: cty.ObjectVal(map[string]cty.Value{ - "list": cty.ListVal([]cty.Value{ - cty.StringVal("string"), - cty.NullVal(cty.String), - }), - }), - Err: true, - }, - { - Cfg: cty.ObjectVal(map[string]cty.Value{ - "map": cty.MapVal(map[string]cty.Value{ - "string": cty.StringVal("string"), - "null": cty.NullVal(cty.String), - }), - }), - Err: false, - }, - { - Cfg: cty.ObjectVal(map[string]cty.Value{ - "object": cty.ObjectVal(map[string]cty.Value{ - "list": cty.ListVal([]cty.Value{ - cty.StringVal("string"), - cty.NullVal(cty.String), - }), - }), - }), - Err: true, - }, - { - Cfg: cty.ObjectVal(map[string]cty.Value{ - "object": cty.ObjectVal(map[string]cty.Value{ - "list": cty.ListVal([]cty.Value{ - cty.StringVal("string"), - cty.NullVal(cty.String), - }), - "list2": cty.ListVal([]cty.Value{ - cty.StringVal("string"), - cty.NullVal(cty.String), - }), - }), - }), - Err: true, - }, - { - Cfg: cty.ObjectVal(map[string]cty.Value{ - "object": cty.ObjectVal(map[string]cty.Value{ - "list": cty.SetVal([]cty.Value{ - cty.StringVal("string"), - cty.NullVal(cty.String), - }), - }), - }), - Err: true, - }, - } { - t.Run(strconv.Itoa(i), func(t *testing.T) { - d := validateConfigNulls(tc.Cfg, nil) - diags := convert.ProtoToDiagnostics(d) - switch { - case tc.Err: - if !diags.HasErrors() { - t.Fatal("expected error") - } - default: - if diags.HasErrors() { - t.Fatalf("unexpected error: %q", diags.Err()) - } - } - }) - } -} diff --git a/helper/plugin/grpc_provisioner.go b/helper/plugin/grpc_provisioner.go deleted file mode 100644 index 088e94e4a..000000000 --- a/helper/plugin/grpc_provisioner.go +++ /dev/null @@ -1,201 +0,0 @@ -package plugin - -import ( - "log" - "strings" - "unicode/utf8" - - "github.com/hashicorp/terraform/helper/schema" - proto "github.com/hashicorp/terraform/internal/tfplugin5" - "github.com/hashicorp/terraform/plugin/convert" - "github.com/hashicorp/terraform/terraform" - "github.com/zclconf/go-cty/cty" - ctyconvert "github.com/zclconf/go-cty/cty/convert" - "github.com/zclconf/go-cty/cty/msgpack" - context "golang.org/x/net/context" -) - -// NewGRPCProvisionerServerShim wraps a terraform.ResourceProvisioner in a -// proto.ProvisionerServer implementation. If the provided provisioner is not a -// *schema.Provisioner, this will return nil, -func NewGRPCProvisionerServerShim(p terraform.ResourceProvisioner) *GRPCProvisionerServer { - sp, ok := p.(*schema.Provisioner) - if !ok { - return nil - } - return &GRPCProvisionerServer{ - provisioner: sp, - } -} - -type GRPCProvisionerServer struct { - provisioner *schema.Provisioner -} - -func (s *GRPCProvisionerServer) GetSchema(_ context.Context, req *proto.GetProvisionerSchema_Request) (*proto.GetProvisionerSchema_Response, error) { - resp := &proto.GetProvisionerSchema_Response{} - - resp.Provisioner = &proto.Schema{ - Block: convert.ConfigSchemaToProto(schema.InternalMap(s.provisioner.Schema).CoreConfigSchema()), - } - - return resp, nil -} - -func (s *GRPCProvisionerServer) ValidateProvisionerConfig(_ context.Context, req *proto.ValidateProvisionerConfig_Request) (*proto.ValidateProvisionerConfig_Response, error) { - resp := &proto.ValidateProvisionerConfig_Response{} - - cfgSchema := schema.InternalMap(s.provisioner.Schema).CoreConfigSchema() - - configVal, err := msgpack.Unmarshal(req.Config.Msgpack, cfgSchema.ImpliedType()) - if err != nil { - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) - return resp, nil - } - - config := terraform.NewResourceConfigShimmed(configVal, cfgSchema) - - warns, errs := s.provisioner.Validate(config) - resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, convert.WarnsAndErrsToProto(warns, errs)) - - return resp, nil -} - -// stringMapFromValue converts a cty.Value to a map[stirng]string. -// This will panic if the val is not a cty.Map(cty.String). -func stringMapFromValue(val cty.Value) map[string]string { - m := map[string]string{} - if val.IsNull() || !val.IsKnown() { - return m - } - - for it := val.ElementIterator(); it.Next(); { - ak, av := it.Element() - name := ak.AsString() - - if !av.IsKnown() || av.IsNull() { - continue - } - - av, _ = ctyconvert.Convert(av, cty.String) - m[name] = av.AsString() - } - - return m -} - -// uiOutput implements the terraform.UIOutput interface to adapt the grpc -// stream to the legacy Provisioner.Apply method. -type uiOutput struct { - srv proto.Provisioner_ProvisionResourceServer -} - -func (o uiOutput) Output(s string) { - err := o.srv.Send(&proto.ProvisionResource_Response{ - Output: toValidUTF8(s, string(utf8.RuneError)), - }) - if err != nil { - log.Printf("[ERROR] %s", err) - } -} - -func (s *GRPCProvisionerServer) ProvisionResource(req *proto.ProvisionResource_Request, srv proto.Provisioner_ProvisionResourceServer) error { - // We send back a diagnostics over the stream if there was a - // provisioner-side problem. - srvResp := &proto.ProvisionResource_Response{} - - cfgSchema := schema.InternalMap(s.provisioner.Schema).CoreConfigSchema() - cfgVal, err := msgpack.Unmarshal(req.Config.Msgpack, cfgSchema.ImpliedType()) - if err != nil { - srvResp.Diagnostics = convert.AppendProtoDiag(srvResp.Diagnostics, err) - srv.Send(srvResp) - return nil - } - resourceConfig := terraform.NewResourceConfigShimmed(cfgVal, cfgSchema) - - connVal, err := msgpack.Unmarshal(req.Connection.Msgpack, cty.Map(cty.String)) - if err != nil { - srvResp.Diagnostics = convert.AppendProtoDiag(srvResp.Diagnostics, err) - srv.Send(srvResp) - return nil - } - - conn := stringMapFromValue(connVal) - - instanceState := &terraform.InstanceState{ - Ephemeral: terraform.EphemeralState{ - ConnInfo: conn, - }, - Meta: make(map[string]interface{}), - } - - err = s.provisioner.Apply(uiOutput{srv}, instanceState, resourceConfig) - if err != nil { - srvResp.Diagnostics = convert.AppendProtoDiag(srvResp.Diagnostics, err) - srv.Send(srvResp) - } - return nil -} - -func (s *GRPCProvisionerServer) Stop(_ context.Context, req *proto.Stop_Request) (*proto.Stop_Response, error) { - resp := &proto.Stop_Response{} - - err := s.provisioner.Stop() - if err != nil { - resp.Error = err.Error() - } - - return resp, nil -} - -// FIXME: backported from go1.13 strings package, remove once terraform is -// using go >= 1.13 -// ToValidUTF8 returns a copy of the string s with each run of invalid UTF-8 byte sequences -// replaced by the replacement string, which may be empty. -func toValidUTF8(s, replacement string) string { - var b strings.Builder - - for i, c := range s { - if c != utf8.RuneError { - continue - } - - _, wid := utf8.DecodeRuneInString(s[i:]) - if wid == 1 { - b.Grow(len(s) + len(replacement)) - b.WriteString(s[:i]) - s = s[i:] - break - } - } - - // Fast path for unchanged input - if b.Cap() == 0 { // didn't call b.Grow above - return s - } - - invalid := false // previous byte was from an invalid UTF-8 sequence - for i := 0; i < len(s); { - c := s[i] - if c < utf8.RuneSelf { - i++ - invalid = false - b.WriteByte(c) - continue - } - _, wid := utf8.DecodeRuneInString(s[i:]) - if wid == 1 { - i++ - if !invalid { - invalid = true - b.WriteString(replacement) - } - continue - } - invalid = false - b.WriteString(s[i : i+wid]) - i += wid - } - - return b.String() -} diff --git a/helper/plugin/grpc_provisioner_test.go b/helper/plugin/grpc_provisioner_test.go deleted file mode 100644 index 9b38daf4a..000000000 --- a/helper/plugin/grpc_provisioner_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package plugin - -import ( - "testing" - "unicode/utf8" - - "github.com/golang/mock/gomock" - "github.com/hashicorp/terraform/helper/schema" - proto "github.com/hashicorp/terraform/internal/tfplugin5" - mockproto "github.com/hashicorp/terraform/plugin/mock_proto" - "github.com/hashicorp/terraform/terraform" - context "golang.org/x/net/context" -) - -var _ proto.ProvisionerServer = (*GRPCProvisionerServer)(nil) - -type validUTF8Matcher string - -func (m validUTF8Matcher) Matches(x interface{}) bool { - resp := x.(*proto.ProvisionResource_Response) - return utf8.Valid([]byte(resp.Output)) -} - -func (m validUTF8Matcher) String() string { - return string(m) -} - -func mockProvisionerServer(t *testing.T, c *gomock.Controller) *mockproto.MockProvisioner_ProvisionResourceServer { - server := mockproto.NewMockProvisioner_ProvisionResourceServer(c) - - server.EXPECT().Send( - validUTF8Matcher("check for valid utf8"), - ).Return(nil) - - return server -} - -// ensure that a provsioner cannot return invalid utf8 which isn't allowed in -// the grpc protocol. -func TestProvisionerInvalidUTF8(t *testing.T) { - p := &schema.Provisioner{ - ConnSchema: map[string]*schema.Schema{ - "foo": { - Type: schema.TypeString, - Optional: true, - }, - }, - - Schema: map[string]*schema.Schema{ - "foo": { - Type: schema.TypeInt, - Optional: true, - }, - }, - - ApplyFunc: func(ctx context.Context) error { - out := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput) - out.Output("invalid \xc3\x28\n") - return nil - }, - } - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - srv := mockProvisionerServer(t, ctrl) - cfg := &proto.DynamicValue{ - Msgpack: []byte("\x81\xa3foo\x01"), - } - conn := &proto.DynamicValue{ - Msgpack: []byte("\x81\xa3foo\xa4host"), - } - provisionerServer := NewGRPCProvisionerServerShim(p) - req := &proto.ProvisionResource_Request{ - Config: cfg, - Connection: conn, - } - - if err := provisionerServer.ProvisionResource(req, srv); err != nil { - t.Fatal(err) - } -} diff --git a/helper/plugin/unknown.go b/helper/plugin/unknown.go deleted file mode 100644 index 64a6784e8..000000000 --- a/helper/plugin/unknown.go +++ /dev/null @@ -1,131 +0,0 @@ -package plugin - -import ( - "fmt" - - "github.com/hashicorp/terraform/configs/configschema" - "github.com/zclconf/go-cty/cty" -) - -// SetUnknowns takes a cty.Value, and compares it to the schema setting any null -// values which are computed to unknown. -func SetUnknowns(val cty.Value, schema *configschema.Block) cty.Value { - if !val.IsKnown() { - return val - } - - // If the object was null, we still need to handle the top level attributes - // which might be computed, but we don't need to expand the blocks. - if val.IsNull() { - objMap := map[string]cty.Value{} - allNull := true - for name, attr := range schema.Attributes { - switch { - case attr.Computed: - objMap[name] = cty.UnknownVal(attr.Type) - allNull = false - default: - objMap[name] = cty.NullVal(attr.Type) - } - } - - // If this object has no unknown attributes, then we can leave it null. - if allNull { - return val - } - - return cty.ObjectVal(objMap) - } - - valMap := val.AsValueMap() - newVals := make(map[string]cty.Value) - - for name, attr := range schema.Attributes { - v := valMap[name] - - if attr.Computed && v.IsNull() { - newVals[name] = cty.UnknownVal(attr.Type) - continue - } - - newVals[name] = v - } - - for name, blockS := range schema.BlockTypes { - blockVal := valMap[name] - if blockVal.IsNull() || !blockVal.IsKnown() { - newVals[name] = blockVal - continue - } - - blockValType := blockVal.Type() - blockElementType := blockS.Block.ImpliedType() - - // This switches on the value type here, so we can correctly switch - // between Tuples/Lists and Maps/Objects. - switch { - case blockS.Nesting == configschema.NestingSingle || blockS.Nesting == configschema.NestingGroup: - // NestingSingle is the only exception here, where we treat the - // block directly as an object - newVals[name] = SetUnknowns(blockVal, &blockS.Block) - - case blockValType.IsSetType(), blockValType.IsListType(), blockValType.IsTupleType(): - listVals := blockVal.AsValueSlice() - newListVals := make([]cty.Value, 0, len(listVals)) - - for _, v := range listVals { - newListVals = append(newListVals, SetUnknowns(v, &blockS.Block)) - } - - switch { - case blockValType.IsSetType(): - switch len(newListVals) { - case 0: - newVals[name] = cty.SetValEmpty(blockElementType) - default: - newVals[name] = cty.SetVal(newListVals) - } - case blockValType.IsListType(): - switch len(newListVals) { - case 0: - newVals[name] = cty.ListValEmpty(blockElementType) - default: - newVals[name] = cty.ListVal(newListVals) - } - case blockValType.IsTupleType(): - newVals[name] = cty.TupleVal(newListVals) - } - - case blockValType.IsMapType(), blockValType.IsObjectType(): - mapVals := blockVal.AsValueMap() - newMapVals := make(map[string]cty.Value) - - for k, v := range mapVals { - newMapVals[k] = SetUnknowns(v, &blockS.Block) - } - - switch { - case blockValType.IsMapType(): - switch len(newMapVals) { - case 0: - newVals[name] = cty.MapValEmpty(blockElementType) - default: - newVals[name] = cty.MapVal(newMapVals) - } - case blockValType.IsObjectType(): - if len(newMapVals) == 0 { - // We need to populate empty values to make a valid object. - for attr, ty := range blockElementType.AttributeTypes() { - newMapVals[attr] = cty.NullVal(ty) - } - } - newVals[name] = cty.ObjectVal(newMapVals) - } - - default: - panic(fmt.Sprintf("failed to set unknown values for nested block %q:%#v", name, blockValType)) - } - } - - return cty.ObjectVal(newVals) -} diff --git a/helper/plugin/unknown_test.go b/helper/plugin/unknown_test.go deleted file mode 100644 index 4214b1849..000000000 --- a/helper/plugin/unknown_test.go +++ /dev/null @@ -1,483 +0,0 @@ -package plugin - -import ( - "testing" - - "github.com/hashicorp/terraform/configs/configschema" - "github.com/zclconf/go-cty/cty" -) - -func TestSetUnknowns(t *testing.T) { - for n, tc := range map[string]struct { - Schema *configschema.Block - Val cty.Value - Expected cty.Value - }{ - "empty": { - &configschema.Block{}, - cty.EmptyObjectVal, - cty.EmptyObjectVal, - }, - "no prior": { - &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "foo": { - Type: cty.String, - Optional: true, - }, - "bar": { - Type: cty.String, - Computed: true, - }, - }, - BlockTypes: map[string]*configschema.NestedBlock{ - "baz": { - Nesting: configschema.NestingSingle, - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "boz": { - Type: cty.String, - Optional: true, - Computed: true, - }, - "biz": { - Type: cty.String, - Optional: true, - Computed: true, - }, - }, - }, - }, - }, - }, - cty.NullVal(cty.Object(map[string]cty.Type{ - "foo": cty.String, - "bar": cty.String, - "baz": cty.Object(map[string]cty.Type{ - "boz": cty.String, - "biz": cty.String, - }), - })), - cty.ObjectVal(map[string]cty.Value{ - "foo": cty.NullVal(cty.String), - "bar": cty.UnknownVal(cty.String), - }), - }, - "null stays null": { - // if the object has no computed attributes, it should stay null - &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "foo": &configschema.Attribute{ - Type: cty.String, - }, - }, - BlockTypes: map[string]*configschema.NestedBlock{ - "baz": { - Nesting: configschema.NestingSet, - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "boz": { - Type: cty.String, - Optional: true, - Computed: true, - }, - }, - }, - }, - }, - }, - cty.NullVal(cty.Object(map[string]cty.Type{ - "foo": cty.String, - "baz": cty.Set(cty.Object(map[string]cty.Type{ - "boz": cty.String, - })), - })), - cty.NullVal(cty.Object(map[string]cty.Type{ - "foo": cty.String, - "baz": cty.Set(cty.Object(map[string]cty.Type{ - "boz": cty.String, - })), - })), - }, - "no prior with set": { - // the set value should remain null - &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "foo": &configschema.Attribute{ - Type: cty.String, - Computed: true, - }, - }, - BlockTypes: map[string]*configschema.NestedBlock{ - "baz": { - Nesting: configschema.NestingSet, - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "boz": { - Type: cty.String, - Optional: true, - Computed: true, - }, - }, - }, - }, - }, - }, - cty.NullVal(cty.Object(map[string]cty.Type{ - "foo": cty.String, - "baz": cty.Set(cty.Object(map[string]cty.Type{ - "boz": cty.String, - })), - })), - cty.ObjectVal(map[string]cty.Value{ - "foo": cty.UnknownVal(cty.String), - }), - }, - "prior attributes": { - &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "foo": { - Type: cty.String, - Optional: true, - }, - "bar": { - Type: cty.String, - Computed: true, - }, - "baz": { - Type: cty.String, - Optional: true, - Computed: true, - }, - "boz": { - Type: cty.String, - Optional: true, - Computed: true, - }, - }, - }, - cty.ObjectVal(map[string]cty.Value{ - "foo": cty.StringVal("bonjour"), - "bar": cty.StringVal("petit dejeuner"), - "baz": cty.StringVal("grande dejeuner"), - }), - cty.ObjectVal(map[string]cty.Value{ - "foo": cty.StringVal("bonjour"), - "bar": cty.StringVal("petit dejeuner"), - "baz": cty.StringVal("grande dejeuner"), - "boz": cty.UnknownVal(cty.String), - }), - }, - "prior nested single": { - &configschema.Block{ - BlockTypes: map[string]*configschema.NestedBlock{ - "foo": { - Nesting: configschema.NestingSingle, - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "bar": { - Type: cty.String, - Optional: true, - Computed: true, - }, - "baz": { - Type: cty.String, - Optional: true, - Computed: true, - }, - }, - }, - }, - }, - }, - cty.ObjectVal(map[string]cty.Value{ - "foo": cty.ObjectVal(map[string]cty.Value{ - "bar": cty.StringVal("beep"), - }), - }), - cty.ObjectVal(map[string]cty.Value{ - "foo": cty.ObjectVal(map[string]cty.Value{ - "bar": cty.StringVal("beep"), - "baz": cty.UnknownVal(cty.String), - }), - }), - }, - "prior nested list": { - &configschema.Block{ - BlockTypes: map[string]*configschema.NestedBlock{ - "foo": { - Nesting: configschema.NestingList, - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "bar": { - Type: cty.String, - Optional: true, - Computed: true, - }, - "baz": { - Type: cty.String, - Optional: true, - Computed: true, - }, - }, - }, - }, - }, - }, - cty.ObjectVal(map[string]cty.Value{ - "foo": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "bar": cty.StringVal("bap"), - }), - cty.ObjectVal(map[string]cty.Value{ - "bar": cty.StringVal("blep"), - }), - }), - }), - cty.ObjectVal(map[string]cty.Value{ - "foo": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "bar": cty.StringVal("bap"), - "baz": cty.UnknownVal(cty.String), - }), - cty.ObjectVal(map[string]cty.Value{ - "bar": cty.StringVal("blep"), - "baz": cty.UnknownVal(cty.String), - }), - }), - }), - }, - "prior nested map": { - &configschema.Block{ - BlockTypes: map[string]*configschema.NestedBlock{ - "foo": { - Nesting: configschema.NestingMap, - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "bar": { - Type: cty.String, - Optional: true, - Computed: true, - }, - "baz": { - Type: cty.String, - Optional: true, - Computed: true, - }, - }, - }, - }, - }, - }, - cty.ObjectVal(map[string]cty.Value{ - "foo": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "bar": cty.NullVal(cty.String), - "baz": cty.StringVal("boop"), - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "bar": cty.StringVal("blep"), - "baz": cty.NullVal(cty.String), - }), - }), - }), - cty.ObjectVal(map[string]cty.Value{ - "foo": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "bar": cty.UnknownVal(cty.String), - "baz": cty.StringVal("boop"), - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "bar": cty.StringVal("blep"), - "baz": cty.UnknownVal(cty.String), - }), - }), - }), - }, - "prior nested set": { - &configschema.Block{ - BlockTypes: map[string]*configschema.NestedBlock{ - "foo": { - Nesting: configschema.NestingSet, - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "bar": { - Type: cty.String, - Optional: true, - }, - "baz": { - Type: cty.String, - Optional: true, - Computed: true, - }, - }, - }, - }, - }, - }, - cty.ObjectVal(map[string]cty.Value{ - "foo": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "bar": cty.StringVal("blep"), - "baz": cty.NullVal(cty.String), - }), - cty.ObjectVal(map[string]cty.Value{ - "bar": cty.StringVal("boop"), - "baz": cty.NullVal(cty.String), - }), - }), - }), - cty.ObjectVal(map[string]cty.Value{ - "foo": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "bar": cty.StringVal("blep"), - "baz": cty.UnknownVal(cty.String), - }), - cty.ObjectVal(map[string]cty.Value{ - "bar": cty.StringVal("boop"), - "baz": cty.UnknownVal(cty.String), - }), - }), - }), - }, - "sets differing only by unknown": { - &configschema.Block{ - BlockTypes: map[string]*configschema.NestedBlock{ - "foo": { - Nesting: configschema.NestingSet, - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "bar": { - Type: cty.String, - Optional: true, - }, - "baz": { - Type: cty.String, - Optional: true, - Computed: true, - }, - }, - }, - }, - }, - }, - cty.ObjectVal(map[string]cty.Value{ - "foo": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "bar": cty.StringVal("boop"), - "baz": cty.NullVal(cty.String), - }), - cty.ObjectVal(map[string]cty.Value{ - "bar": cty.StringVal("boop"), - "baz": cty.UnknownVal(cty.String), - }), - }), - }), - cty.ObjectVal(map[string]cty.Value{ - "foo": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "bar": cty.StringVal("boop"), - "baz": cty.UnknownVal(cty.String), - }), - cty.ObjectVal(map[string]cty.Value{ - "bar": cty.StringVal("boop"), - "baz": cty.UnknownVal(cty.String), - }), - }), - }), - }, - "prior nested list with dynamic": { - &configschema.Block{ - BlockTypes: map[string]*configschema.NestedBlock{ - "foo": { - Nesting: configschema.NestingList, - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "bar": { - Type: cty.String, - Optional: true, - Computed: true, - }, - "baz": { - Type: cty.DynamicPseudoType, - Optional: true, - Computed: true, - }, - }, - }, - }, - }, - }, - cty.ObjectVal(map[string]cty.Value{ - "foo": cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "bar": cty.NullVal(cty.String), - "baz": cty.NumberIntVal(8), - }), - }), - }), - cty.ObjectVal(map[string]cty.Value{ - "foo": cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "bar": cty.UnknownVal(cty.String), - "baz": cty.NumberIntVal(8), - }), - }), - }), - }, - "prior nested map with dynamic": { - &configschema.Block{ - BlockTypes: map[string]*configschema.NestedBlock{ - "foo": { - Nesting: configschema.NestingMap, - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "bar": { - Type: cty.String, - Optional: true, - Computed: true, - }, - "baz": { - Type: cty.DynamicPseudoType, - Optional: true, - Computed: true, - }, - }, - }, - }, - }, - }, - cty.ObjectVal(map[string]cty.Value{ - "foo": cty.ObjectVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "bar": cty.StringVal("beep"), - "baz": cty.NullVal(cty.DynamicPseudoType), - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "bar": cty.StringVal("boop"), - "baz": cty.NumberIntVal(8), - }), - }), - }), - cty.ObjectVal(map[string]cty.Value{ - "foo": cty.ObjectVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "bar": cty.StringVal("beep"), - "baz": cty.UnknownVal(cty.DynamicPseudoType), - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "bar": cty.StringVal("boop"), - "baz": cty.NumberIntVal(8), - }), - }), - }), - }, - } { - t.Run(n, func(t *testing.T) { - got := SetUnknowns(tc.Val, tc.Schema) - if !got.RawEquals(tc.Expected) { - t.Fatalf("\nexpected: %#v\ngot: %#v\n", tc.Expected, got) - } - }) - } -}