github.com/crossplane/upjet@v1.3.0/pkg/controller/external_tfpluginsdk.go (about)

     1  // SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io>
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package controller
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"strings"
    11  	"time"
    12  
    13  	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
    14  	"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
    15  	"github.com/crossplane/crossplane-runtime/pkg/logging"
    16  	"github.com/crossplane/crossplane-runtime/pkg/meta"
    17  	"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
    18  	xpresource "github.com/crossplane/crossplane-runtime/pkg/resource"
    19  	"github.com/hashicorp/go-cty/cty"
    20  	tfdiag "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
    21  	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
    22  	tf "github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
    23  	"github.com/pkg/errors"
    24  	corev1 "k8s.io/api/core/v1"
    25  	"k8s.io/apimachinery/pkg/util/sets"
    26  	"sigs.k8s.io/controller-runtime/pkg/client"
    27  
    28  	"github.com/crossplane/upjet/pkg/config"
    29  	"github.com/crossplane/upjet/pkg/metrics"
    30  	"github.com/crossplane/upjet/pkg/resource"
    31  	"github.com/crossplane/upjet/pkg/resource/json"
    32  	"github.com/crossplane/upjet/pkg/terraform"
    33  )
    34  
    35  type TerraformPluginSDKConnector struct {
    36  	getTerraformSetup           terraform.SetupFn
    37  	kube                        client.Client
    38  	config                      *config.Resource
    39  	logger                      logging.Logger
    40  	metricRecorder              *metrics.MetricRecorder
    41  	operationTrackerStore       *OperationTrackerStore
    42  	isManagementPoliciesEnabled bool
    43  }
    44  
    45  // TerraformPluginSDKOption allows you to configure TerraformPluginSDKConnector.
    46  type TerraformPluginSDKOption func(connector *TerraformPluginSDKConnector)
    47  
    48  // WithTerraformPluginSDKLogger configures a logger for the TerraformPluginSDKConnector.
    49  func WithTerraformPluginSDKLogger(l logging.Logger) TerraformPluginSDKOption {
    50  	return func(c *TerraformPluginSDKConnector) {
    51  		c.logger = l
    52  	}
    53  }
    54  
    55  // WithTerraformPluginSDKMetricRecorder configures a metrics.MetricRecorder for the
    56  // TerraformPluginSDKConnector.
    57  func WithTerraformPluginSDKMetricRecorder(r *metrics.MetricRecorder) TerraformPluginSDKOption {
    58  	return func(c *TerraformPluginSDKConnector) {
    59  		c.metricRecorder = r
    60  	}
    61  }
    62  
    63  // WithTerraformPluginSDKManagementPolicies configures whether the client should
    64  // handle management policies.
    65  func WithTerraformPluginSDKManagementPolicies(isManagementPoliciesEnabled bool) TerraformPluginSDKOption {
    66  	return func(c *TerraformPluginSDKConnector) {
    67  		c.isManagementPoliciesEnabled = isManagementPoliciesEnabled
    68  	}
    69  }
    70  
    71  // NewTerraformPluginSDKConnector initializes a new TerraformPluginSDKConnector
    72  func NewTerraformPluginSDKConnector(kube client.Client, sf terraform.SetupFn, cfg *config.Resource, ots *OperationTrackerStore, opts ...TerraformPluginSDKOption) *TerraformPluginSDKConnector {
    73  	nfc := &TerraformPluginSDKConnector{
    74  		kube:                  kube,
    75  		getTerraformSetup:     sf,
    76  		config:                cfg,
    77  		operationTrackerStore: ots,
    78  	}
    79  	for _, f := range opts {
    80  		f(nfc)
    81  	}
    82  	return nfc
    83  }
    84  
    85  func copyParameters(tfState, params map[string]any) map[string]any {
    86  	targetState := make(map[string]any, len(params))
    87  	for k, v := range params {
    88  		targetState[k] = v
    89  	}
    90  	for k, v := range tfState {
    91  		targetState[k] = v
    92  	}
    93  	return targetState
    94  }
    95  
    96  func getJSONMap(mg xpresource.Managed) (map[string]any, error) {
    97  	pv, err := fieldpath.PaveObject(mg)
    98  	if err != nil {
    99  		return nil, errors.Wrap(err, "cannot pave the managed resource")
   100  	}
   101  	v, err := pv.GetValue("spec.forProvider")
   102  	if err != nil {
   103  		return nil, errors.Wrap(err, "cannot get spec.forProvider value from paved object")
   104  	}
   105  	return v.(map[string]any), nil
   106  }
   107  
   108  type Resource interface {
   109  	Apply(ctx context.Context, s *tf.InstanceState, d *tf.InstanceDiff, meta interface{}) (*tf.InstanceState, tfdiag.Diagnostics)
   110  	RefreshWithoutUpgrade(ctx context.Context, s *tf.InstanceState, meta interface{}) (*tf.InstanceState, tfdiag.Diagnostics)
   111  }
   112  
   113  type terraformPluginSDKExternal struct {
   114  	ts             terraform.Setup
   115  	resourceSchema Resource
   116  	config         *config.Resource
   117  	instanceDiff   *tf.InstanceDiff
   118  	params         map[string]any
   119  	rawConfig      cty.Value
   120  	logger         logging.Logger
   121  	metricRecorder *metrics.MetricRecorder
   122  	opTracker      *AsyncTracker
   123  }
   124  
   125  func getExtendedParameters(ctx context.Context, tr resource.Terraformed, externalName string, config *config.Resource, ts terraform.Setup, initParamsMerged bool, kube client.Client) (map[string]any, error) {
   126  	params, err := tr.GetMergedParameters(initParamsMerged)
   127  	if err != nil {
   128  		return nil, errors.Wrap(err, "cannot get merged parameters")
   129  	}
   130  	if err = resource.GetSensitiveParameters(ctx, &APISecretClient{kube: kube}, tr, params, tr.GetConnectionDetailsMapping()); err != nil {
   131  		return nil, errors.Wrap(err, "cannot store sensitive parameters into params")
   132  	}
   133  	config.ExternalName.SetIdentifierArgumentFn(params, externalName)
   134  	if config.TerraformConfigurationInjector != nil {
   135  		m, err := getJSONMap(tr)
   136  		if err != nil {
   137  			return nil, errors.Wrap(err, "cannot get JSON map for the managed resource's spec.forProvider value")
   138  		}
   139  		if err := config.TerraformConfigurationInjector(m, params); err != nil {
   140  			return nil, errors.Wrap(err, "cannot invoke the configured TerraformConfigurationInjector")
   141  		}
   142  	}
   143  
   144  	tfID, err := config.ExternalName.GetIDFn(ctx, externalName, params, ts.Map())
   145  	if err != nil {
   146  		return nil, errors.Wrap(err, "cannot get ID")
   147  	}
   148  	params["id"] = tfID
   149  	// we need to parameterize the following for a provider
   150  	// not all providers may have this attribute
   151  	// TODO: tags-tags_all implementation is AWS specific.
   152  	// Consider making this logic independent of provider.
   153  	if config.TerraformResource != nil {
   154  		if _, ok := config.TerraformResource.CoreConfigSchema().Attributes["tags_all"]; ok {
   155  			params["tags_all"] = params["tags"]
   156  		}
   157  	}
   158  	return params, nil
   159  }
   160  
   161  func (c *TerraformPluginSDKConnector) processParamsWithHCLParser(schemaMap map[string]*schema.Schema, params map[string]any) map[string]any {
   162  	if params == nil {
   163  		return params
   164  	}
   165  	for key, param := range params {
   166  		if sc, ok := schemaMap[key]; ok {
   167  			params[key] = c.applyHCLParserToParam(sc, param)
   168  		} else {
   169  			params[key] = param
   170  		}
   171  	}
   172  	return params
   173  }
   174  
   175  func (c *TerraformPluginSDKConnector) applyHCLParserToParam(sc *schema.Schema, param any) any { //nolint:gocyclo
   176  	if param == nil {
   177  		return param
   178  	}
   179  	switch sc.Type { //nolint:exhaustive
   180  	case schema.TypeMap:
   181  		if sc.Elem == nil {
   182  			return param
   183  		}
   184  		pmap, okParam := param.(map[string]any)
   185  		// TypeMap only supports schema in Elem
   186  		if mapSchema, ok := sc.Elem.(*schema.Schema); ok && okParam {
   187  			for pk, pv := range pmap {
   188  				pmap[pk] = c.applyHCLParserToParam(mapSchema, pv)
   189  			}
   190  			return pmap
   191  		}
   192  	case schema.TypeSet, schema.TypeList:
   193  		if sc.Elem == nil {
   194  			return param
   195  		}
   196  		pArray, okParam := param.([]any)
   197  		if setSchema, ok := sc.Elem.(*schema.Schema); ok && okParam {
   198  			for i, p := range pArray {
   199  				pArray[i] = c.applyHCLParserToParam(setSchema, p)
   200  			}
   201  			return pArray
   202  		} else if setResource, ok := sc.Elem.(*schema.Resource); ok {
   203  			for i, p := range pArray {
   204  				if resParam, okRParam := p.(map[string]any); okRParam {
   205  					pArray[i] = c.processParamsWithHCLParser(setResource.Schema, resParam)
   206  				}
   207  			}
   208  		}
   209  	case schema.TypeString:
   210  		// For String types check if it is an HCL string and process
   211  		if isHCLSnippetPattern.MatchString(param.(string)) {
   212  			hclProccessedParam, err := processHCLParam(param.(string))
   213  			if err != nil {
   214  				c.logger.Debug("could not process param, returning original", "param", sc.GoString())
   215  			} else {
   216  				param = hclProccessedParam
   217  			}
   218  		}
   219  		return param
   220  	default:
   221  		return param
   222  	}
   223  	return param
   224  }
   225  
   226  func (c *TerraformPluginSDKConnector) Connect(ctx context.Context, mg xpresource.Managed) (managed.ExternalClient, error) { //nolint:gocyclo
   227  	c.metricRecorder.ObserveReconcileDelay(mg.GetObjectKind().GroupVersionKind(), mg.GetName())
   228  	logger := c.logger.WithValues("uid", mg.GetUID(), "name", mg.GetName(), "gvk", mg.GetObjectKind().GroupVersionKind().String())
   229  	logger.Debug("Connecting to the service provider")
   230  	start := time.Now()
   231  	ts, err := c.getTerraformSetup(ctx, c.kube, mg)
   232  	metrics.ExternalAPITime.WithLabelValues("connect").Observe(time.Since(start).Seconds())
   233  	if err != nil {
   234  		return nil, errors.Wrap(err, errGetTerraformSetup)
   235  	}
   236  
   237  	// To Compute the ResourceDiff: n.resourceSchema.Diff(...)
   238  	tr := mg.(resource.Terraformed)
   239  	opTracker := c.operationTrackerStore.Tracker(tr)
   240  	externalName := meta.GetExternalName(tr)
   241  	params, err := getExtendedParameters(ctx, tr, externalName, c.config, ts, c.isManagementPoliciesEnabled, c.kube)
   242  	if err != nil {
   243  		return nil, errors.Wrapf(err, "failed to get the extended parameters for resource %q", mg.GetName())
   244  	}
   245  	params = c.processParamsWithHCLParser(c.config.TerraformResource.Schema, params)
   246  
   247  	schemaBlock := c.config.TerraformResource.CoreConfigSchema()
   248  	rawConfig, err := schema.JSONMapToStateValue(params, schemaBlock)
   249  	if err != nil {
   250  		return nil, errors.Wrap(err, "failed to convert params JSON map to cty.Value")
   251  	}
   252  	if !opTracker.HasState() {
   253  		logger.Debug("Instance state not found in cache, reconstructing...")
   254  		tfState, err := tr.GetObservation()
   255  		if err != nil {
   256  			return nil, errors.Wrap(err, "failed to get the observation")
   257  		}
   258  		copyParams := len(tfState) == 0
   259  		if err = resource.GetSensitiveParameters(ctx, &APISecretClient{kube: c.kube}, tr, tfState, tr.GetConnectionDetailsMapping()); err != nil {
   260  			return nil, errors.Wrap(err, "cannot store sensitive parameters into tfState")
   261  		}
   262  		c.config.ExternalName.SetIdentifierArgumentFn(tfState, externalName)
   263  		tfState["id"] = params["id"]
   264  		if copyParams {
   265  			tfState = copyParameters(tfState, params)
   266  		}
   267  
   268  		tfStateCtyValue, err := schema.JSONMapToStateValue(tfState, schemaBlock)
   269  		if err != nil {
   270  			return nil, errors.Wrap(err, "cannot convert JSON map to state cty.Value")
   271  		}
   272  		s, err := c.config.TerraformResource.ShimInstanceStateFromValue(tfStateCtyValue)
   273  		if err != nil {
   274  			return nil, errors.Wrap(err, "failed to convert cty.Value to terraform.InstanceState")
   275  		}
   276  		s.RawPlan = tfStateCtyValue
   277  		s.RawConfig = rawConfig
   278  
   279  		timeouts := getTimeoutParameters(c.config)
   280  		if len(timeouts) > 0 {
   281  			if s == nil {
   282  				s = &tf.InstanceState{}
   283  			}
   284  			if s.Meta == nil {
   285  				s.Meta = make(map[string]interface{})
   286  			}
   287  			s.Meta[schema.TimeoutKey] = timeouts
   288  		}
   289  		opTracker.SetTfState(s)
   290  	}
   291  
   292  	return &terraformPluginSDKExternal{
   293  		ts:             ts,
   294  		resourceSchema: c.config.TerraformResource,
   295  		config:         c.config,
   296  		params:         params,
   297  		rawConfig:      rawConfig,
   298  		logger:         logger,
   299  		metricRecorder: c.metricRecorder,
   300  		opTracker:      opTracker,
   301  	}, nil
   302  }
   303  
   304  func filterInitExclusiveDiffs(tr resource.Terraformed, instanceDiff *tf.InstanceDiff) error { //nolint:gocyclo
   305  	if instanceDiff == nil || instanceDiff.Empty() {
   306  		return nil
   307  	}
   308  
   309  	paramsForProvider, err := tr.GetParameters()
   310  	if err != nil {
   311  		return errors.Wrap(err, "cannot get spec.forProvider parameters")
   312  	}
   313  	paramsInitProvider, err := tr.GetInitParameters()
   314  	if err != nil {
   315  		return errors.Wrap(err, "cannot get spec.initProvider parameters")
   316  	}
   317  
   318  	initProviderExclusiveParamKeys := getTerraformIgnoreChanges(paramsForProvider, paramsInitProvider)
   319  	for _, keyToIgnore := range initProviderExclusiveParamKeys {
   320  		for attributeKey := range instanceDiff.Attributes {
   321  			keyToIgnoreAsPrefix := fmt.Sprintf("%s.", keyToIgnore)
   322  			if keyToIgnore != attributeKey && !strings.HasPrefix(attributeKey, keyToIgnoreAsPrefix) {
   323  				continue
   324  			}
   325  
   326  			delete(instanceDiff.Attributes, attributeKey)
   327  
   328  			// TODO: tags-tags_all implementation is AWS specific.
   329  			// Consider making this logic independent of provider.
   330  			keyComponents := strings.Split(attributeKey, ".")
   331  			if keyComponents[0] != "tags" {
   332  				continue
   333  			}
   334  			keyComponents[0] = "tags_all"
   335  			tagsAllAttributeKey := strings.Join(keyComponents, ".")
   336  			delete(instanceDiff.Attributes, tagsAllAttributeKey)
   337  		}
   338  	}
   339  
   340  	// Delete length keys, such as "tags.%" (schema.TypeMap) and
   341  	// "cidrBlocks.#" (schema.TypeSet), because of two reasons:
   342  	//
   343  	// 1. Diffs are applied successfully without them, except for
   344  	// schema.TypeList.
   345  	//
   346  	// 2. If only length keys remain in the diff, after ignored
   347  	// attributes are removed above, they cause diff to be considered
   348  	// non-empty, even though it is effectively empty, therefore causing
   349  	// an infinite update loop.
   350  	for _, keyToIgnore := range initProviderExclusiveParamKeys {
   351  		keyComponents := strings.Split(keyToIgnore, ".")
   352  		if len(keyComponents) < 2 {
   353  			continue
   354  		}
   355  
   356  		// TODO: Consider locating the schema corresponding to keyToIgnore
   357  		// and checking whether it's a collection, before attempting to
   358  		// delete its length key.
   359  		for _, lengthSymbol := range []string{"%", "#"} {
   360  			keyComponents[len(keyComponents)-1] = lengthSymbol
   361  			lengthKey := strings.Join(keyComponents, ".")
   362  			delete(instanceDiff.Attributes, lengthKey)
   363  		}
   364  
   365  		// TODO: tags-tags_all implementation is AWS specific.
   366  		// Consider making this logic independent of provider.
   367  		if keyComponents[0] == "tags" {
   368  			keyComponents[0] = "tags_all"
   369  			keyComponents[len(keyComponents)-1] = "%"
   370  			lengthKey := strings.Join(keyComponents, ".")
   371  			delete(instanceDiff.Attributes, lengthKey)
   372  		}
   373  	}
   374  	return nil
   375  }
   376  
   377  // resource timeouts configuration
   378  func getTimeoutParameters(config *config.Resource) map[string]any { //nolint:gocyclo
   379  	timeouts := make(map[string]any)
   380  	// first use the timeout overrides specified in
   381  	// the Terraform resource schema
   382  	if config.TerraformResource.Timeouts != nil {
   383  		if config.TerraformResource.Timeouts.Create != nil && *config.TerraformResource.Timeouts.Create != 0 {
   384  			timeouts[schema.TimeoutCreate] = config.TerraformResource.Timeouts.Create.Nanoseconds()
   385  		}
   386  		if config.TerraformResource.Timeouts.Update != nil && *config.TerraformResource.Timeouts.Update != 0 {
   387  			timeouts[schema.TimeoutUpdate] = config.TerraformResource.Timeouts.Update.Nanoseconds()
   388  		}
   389  		if config.TerraformResource.Timeouts.Delete != nil && *config.TerraformResource.Timeouts.Delete != 0 {
   390  			timeouts[schema.TimeoutDelete] = config.TerraformResource.Timeouts.Delete.Nanoseconds()
   391  		}
   392  		if config.TerraformResource.Timeouts.Read != nil && *config.TerraformResource.Timeouts.Read != 0 {
   393  			timeouts[schema.TimeoutRead] = config.TerraformResource.Timeouts.Read.Nanoseconds()
   394  		}
   395  	}
   396  	// then, override any Terraform defaults using any upjet
   397  	// resource configuration overrides
   398  	if config.OperationTimeouts.Create != 0 {
   399  		timeouts[schema.TimeoutCreate] = config.OperationTimeouts.Create.Nanoseconds()
   400  	}
   401  	if config.OperationTimeouts.Update != 0 {
   402  		timeouts[schema.TimeoutUpdate] = config.OperationTimeouts.Update.Nanoseconds()
   403  	}
   404  	if config.OperationTimeouts.Delete != 0 {
   405  		timeouts[schema.TimeoutDelete] = config.OperationTimeouts.Delete.Nanoseconds()
   406  	}
   407  	if config.OperationTimeouts.Read != 0 {
   408  		timeouts[schema.TimeoutRead] = config.OperationTimeouts.Read.Nanoseconds()
   409  	}
   410  	return timeouts
   411  }
   412  
   413  func (n *terraformPluginSDKExternal) getResourceDataDiff(tr resource.Terraformed, ctx context.Context, s *tf.InstanceState, resourceExists bool) (*tf.InstanceDiff, error) { //nolint:gocyclo
   414  	resourceConfig := tf.NewResourceConfigRaw(n.params)
   415  	instanceDiff, err := schema.InternalMap(n.config.TerraformResource.Schema).Diff(ctx, s, resourceConfig, n.config.TerraformResource.CustomizeDiff, n.ts.Meta, false)
   416  	if err != nil {
   417  		return nil, errors.Wrap(err, "failed to get *terraform.InstanceDiff")
   418  	}
   419  	if n.config.TerraformCustomDiff != nil {
   420  		instanceDiff, err = n.config.TerraformCustomDiff(instanceDiff, s, resourceConfig)
   421  		if err != nil {
   422  			return nil, errors.Wrap(err, "failed to compute the customized terraform.InstanceDiff")
   423  		}
   424  	}
   425  
   426  	if resourceExists {
   427  		if err := filterInitExclusiveDiffs(tr, instanceDiff); err != nil {
   428  			return nil, errors.Wrap(err, "failed to filter the diffs exclusive to spec.initProvider in the terraform.InstanceDiff")
   429  		}
   430  	}
   431  	if instanceDiff != nil {
   432  		v := cty.EmptyObjectVal
   433  		v, err = instanceDiff.ApplyToValue(v, n.config.TerraformResource.CoreConfigSchema())
   434  		if err != nil {
   435  			return nil, errors.Wrap(err, "cannot apply Terraform instance diff to an empty value")
   436  		}
   437  		instanceDiff.RawPlan = v
   438  	}
   439  	if instanceDiff != nil && !instanceDiff.Empty() {
   440  		n.logger.Debug("Diff detected", "instanceDiff", instanceDiff.GoString())
   441  		// Assumption: Source of truth when applying diffs, for instance on updates, is instanceDiff.Attributes.
   442  		// Setting instanceDiff.RawConfig has no effect on diff application.
   443  		instanceDiff.RawConfig = n.rawConfig
   444  	}
   445  	timeouts := getTimeoutParameters(n.config)
   446  	if len(timeouts) > 0 {
   447  		if instanceDiff == nil {
   448  			instanceDiff = tf.NewInstanceDiff()
   449  		}
   450  		if instanceDiff.Meta == nil {
   451  			instanceDiff.Meta = make(map[string]interface{})
   452  		}
   453  		instanceDiff.Meta[schema.TimeoutKey] = timeouts
   454  	}
   455  	return instanceDiff, nil
   456  }
   457  
   458  func (n *terraformPluginSDKExternal) Observe(ctx context.Context, mg xpresource.Managed) (managed.ExternalObservation, error) { //nolint:gocyclo
   459  	n.logger.Debug("Observing the external resource")
   460  
   461  	if meta.WasDeleted(mg) && n.opTracker.IsDeleted() {
   462  		return managed.ExternalObservation{
   463  			ResourceExists: false,
   464  		}, nil
   465  	}
   466  
   467  	start := time.Now()
   468  	newState, diag := n.resourceSchema.RefreshWithoutUpgrade(ctx, n.opTracker.GetTfState(), n.ts.Meta)
   469  	metrics.ExternalAPITime.WithLabelValues("read").Observe(time.Since(start).Seconds())
   470  	if diag != nil && diag.HasError() {
   471  		return managed.ExternalObservation{}, errors.Errorf("failed to observe the resource: %v", diag)
   472  	}
   473  	diffState := n.opTracker.GetTfState()
   474  	n.opTracker.SetTfState(newState) // TODO: missing RawConfig & RawPlan here...
   475  	resourceExists := newState != nil && newState.ID != ""
   476  
   477  	var stateValueMap map[string]any
   478  	if resourceExists {
   479  		jsonMap, stateValue, err := n.fromInstanceStateToJSONMap(newState)
   480  		if err != nil {
   481  			return managed.ExternalObservation{}, errors.Wrap(err, "cannot convert instance state to JSON map")
   482  		}
   483  		stateValueMap = jsonMap
   484  		newState.RawPlan = stateValue
   485  		newState.RawConfig = n.rawConfig
   486  		diffState = newState
   487  	} else if diffState != nil {
   488  		diffState.Attributes = nil
   489  		diffState.ID = ""
   490  	}
   491  	instanceDiff, err := n.getResourceDataDiff(mg.(resource.Terraformed), ctx, diffState, resourceExists)
   492  	if err != nil {
   493  		return managed.ExternalObservation{}, errors.Wrap(err, "cannot compute the instance diff")
   494  	}
   495  	if instanceDiff == nil {
   496  		instanceDiff = tf.NewInstanceDiff()
   497  	}
   498  	n.instanceDiff = instanceDiff
   499  	noDiff := instanceDiff.Empty()
   500  
   501  	var connDetails managed.ConnectionDetails
   502  	if !resourceExists && mg.GetDeletionTimestamp() != nil {
   503  		gvk := mg.GetObjectKind().GroupVersionKind()
   504  		metrics.DeletionTime.WithLabelValues(gvk.Group, gvk.Version, gvk.Kind).Observe(time.Since(mg.GetDeletionTimestamp().Time).Seconds())
   505  	}
   506  	specUpdateRequired := false
   507  	if resourceExists {
   508  		if mg.GetCondition(xpv1.TypeReady).Status == corev1.ConditionUnknown ||
   509  			mg.GetCondition(xpv1.TypeReady).Status == corev1.ConditionFalse {
   510  			addTTR(mg)
   511  		}
   512  		mg.SetConditions(xpv1.Available())
   513  
   514  		buff, err := json.TFParser.Marshal(stateValueMap)
   515  		if err != nil {
   516  			return managed.ExternalObservation{}, errors.Wrap(err, "cannot marshal the attributes of the new state for late-initialization")
   517  		}
   518  
   519  		policySet := sets.New[xpv1.ManagementAction](mg.(resource.Terraformed).GetManagementPolicies()...)
   520  		policyHasLateInit := policySet.HasAny(xpv1.ManagementActionLateInitialize, xpv1.ManagementActionAll)
   521  		if policyHasLateInit {
   522  			specUpdateRequired, err = mg.(resource.Terraformed).LateInitialize(buff)
   523  			if err != nil {
   524  				return managed.ExternalObservation{}, errors.Wrap(err, "cannot late-initialize the managed resource")
   525  			}
   526  		}
   527  
   528  		err = mg.(resource.Terraformed).SetObservation(stateValueMap)
   529  		if err != nil {
   530  			return managed.ExternalObservation{}, errors.Errorf("could not set observation: %v", err)
   531  		}
   532  		connDetails, err = resource.GetConnectionDetails(stateValueMap, mg.(resource.Terraformed), n.config)
   533  		if err != nil {
   534  			return managed.ExternalObservation{}, errors.Wrap(err, "cannot get connection details")
   535  		}
   536  
   537  		if noDiff {
   538  			n.metricRecorder.SetReconcileTime(mg.GetName())
   539  		}
   540  		if !specUpdateRequired {
   541  			resource.SetUpToDateCondition(mg, noDiff)
   542  		}
   543  		// check for an external-name change
   544  		if nameChanged, err := n.setExternalName(mg, stateValueMap); err != nil {
   545  			return managed.ExternalObservation{}, errors.Wrapf(err, "failed to set the external-name of the managed resource during observe")
   546  		} else {
   547  			specUpdateRequired = specUpdateRequired || nameChanged
   548  		}
   549  	}
   550  
   551  	return managed.ExternalObservation{
   552  		ResourceExists:          resourceExists,
   553  		ResourceUpToDate:        noDiff,
   554  		ConnectionDetails:       connDetails,
   555  		ResourceLateInitialized: specUpdateRequired,
   556  	}, nil
   557  }
   558  
   559  // sets the external-name on the MR. Returns `true`
   560  // if the external-name of the MR has changed.
   561  func (n *terraformPluginSDKExternal) setExternalName(mg xpresource.Managed, stateValueMap map[string]interface{}) (bool, error) {
   562  	id, ok := stateValueMap["id"]
   563  	if !ok || id.(string) == "" {
   564  		return false, nil
   565  	}
   566  	newName, err := n.config.ExternalName.GetExternalNameFn(stateValueMap)
   567  	if err != nil {
   568  		return false, errors.Wrapf(err, "failed to compute the external-name from the state map of the resource with the ID %s", id)
   569  	}
   570  	oldName := meta.GetExternalName(mg)
   571  	// we have to make sure the newly set external-name is recorded
   572  	meta.SetExternalName(mg, newName)
   573  	return oldName != newName, nil
   574  }
   575  
   576  func (n *terraformPluginSDKExternal) Create(ctx context.Context, mg xpresource.Managed) (managed.ExternalCreation, error) {
   577  	n.logger.Debug("Creating the external resource")
   578  	start := time.Now()
   579  	newState, diag := n.resourceSchema.Apply(ctx, n.opTracker.GetTfState(), n.instanceDiff, n.ts.Meta)
   580  	metrics.ExternalAPITime.WithLabelValues("create").Observe(time.Since(start).Seconds())
   581  	if diag != nil && diag.HasError() {
   582  		// we need to store the Terraform state from the downstream create call if
   583  		// one is available even if the diagnostics has reported errors.
   584  		// The downstream create call comprises multiple external API calls such as
   585  		// the external resource create call, expected state assertion calls
   586  		// (external resource state reads) and external resource state refresh
   587  		// calls, etc. Any of these steps can fail and if the initial
   588  		// external resource create call succeeds, then the TF plugin SDK makes the
   589  		// state (together with the TF ID associated with the external resource)
   590  		// available reporting any encountered issues in the returned diagnostics.
   591  		// If we don't record the returned state from the successful create call,
   592  		// then we may hit issues for resources whose Crossplane identifiers cannot
   593  		// be computed solely from spec parameters and provider configs, i.e.,
   594  		// those that contain a random part generated by the CSP. Please see:
   595  		// https://github.com/upbound/provider-aws/issues/1010, or
   596  		// https://github.com/upbound/provider-aws/issues/1018, which both involve
   597  		// MRs with config.IdentifierFromProvider external-name configurations.
   598  		// NOTE: The safe (and thus the proper) thing to do in this situation from
   599  		// the Crossplane provider's perspective is to set the MR's
   600  		// `crossplane.io/external-create-failed` annotation because the provider
   601  		// does not know the exact state the external resource is in and a manual
   602  		// intervention may be required. But at the time we are introducing this
   603  		// fix, we believe associating the external-resource with the MR will just
   604  		// provide a better UX although the external resource may not be in the
   605  		// expected/desired state yet. We are also planning for improvements on the
   606  		// crossplane-runtime's managed reconciler to better support upjet's async
   607  		// operations in this regard.
   608  		if !n.opTracker.HasState() { // we do not expect a previous state here but just being defensive
   609  			n.opTracker.SetTfState(newState)
   610  		}
   611  		return managed.ExternalCreation{}, errors.Errorf("failed to create the resource: %v", diag)
   612  	}
   613  
   614  	if newState == nil || newState.ID == "" {
   615  		return managed.ExternalCreation{}, errors.New("failed to read the ID of the new resource")
   616  	}
   617  	n.opTracker.SetTfState(newState)
   618  
   619  	stateValueMap, _, err := n.fromInstanceStateToJSONMap(newState)
   620  	if err != nil {
   621  		return managed.ExternalCreation{}, errors.Wrap(err, "failed to convert instance state to map")
   622  	}
   623  	if _, err := n.setExternalName(mg, stateValueMap); err != nil {
   624  		return managed.ExternalCreation{}, errors.Wrap(err, "failed to set the external-name of the managed resource during create")
   625  	}
   626  	err = mg.(resource.Terraformed).SetObservation(stateValueMap)
   627  	if err != nil {
   628  		return managed.ExternalCreation{}, errors.Errorf("could not set observation: %v", err)
   629  	}
   630  	conn, err := resource.GetConnectionDetails(stateValueMap, mg.(resource.Terraformed), n.config)
   631  	if err != nil {
   632  		return managed.ExternalCreation{}, errors.Wrap(err, "cannot get connection details")
   633  	}
   634  
   635  	return managed.ExternalCreation{ConnectionDetails: conn}, nil
   636  }
   637  
   638  func (n *terraformPluginSDKExternal) assertNoForceNew() error {
   639  	if n.instanceDiff == nil {
   640  		return nil
   641  	}
   642  	for k, ad := range n.instanceDiff.Attributes {
   643  		if ad == nil {
   644  			continue
   645  		}
   646  		// TODO: use a multi-error implementation to report changes to
   647  		//  all `ForceNew` arguments.
   648  		if ad.RequiresNew {
   649  			if ad.Sensitive {
   650  				return errors.Errorf("cannot change the value of the argument %q", k)
   651  			}
   652  			return errors.Errorf("cannot change the value of the argument %q from %q to %q", k, ad.Old, ad.New)
   653  		}
   654  	}
   655  	return nil
   656  }
   657  
   658  func (n *terraformPluginSDKExternal) Update(ctx context.Context, mg xpresource.Managed) (managed.ExternalUpdate, error) {
   659  	n.logger.Debug("Updating the external resource")
   660  
   661  	if err := n.assertNoForceNew(); err != nil {
   662  		return managed.ExternalUpdate{}, errors.Wrap(err, "refuse to update the external resource because the following update requires replacing it")
   663  	}
   664  
   665  	start := time.Now()
   666  	newState, diag := n.resourceSchema.Apply(ctx, n.opTracker.GetTfState(), n.instanceDiff, n.ts.Meta)
   667  	metrics.ExternalAPITime.WithLabelValues("update").Observe(time.Since(start).Seconds())
   668  	if diag != nil && diag.HasError() {
   669  		return managed.ExternalUpdate{}, errors.Errorf("failed to update the resource: %v", diag)
   670  	}
   671  	n.opTracker.SetTfState(newState)
   672  
   673  	stateValueMap, _, err := n.fromInstanceStateToJSONMap(newState)
   674  	if err != nil {
   675  		return managed.ExternalUpdate{}, err
   676  	}
   677  
   678  	err = mg.(resource.Terraformed).SetObservation(stateValueMap)
   679  	if err != nil {
   680  		return managed.ExternalUpdate{}, errors.Errorf("failed to set observation: %v", err)
   681  	}
   682  	return managed.ExternalUpdate{}, nil
   683  }
   684  
   685  func (n *terraformPluginSDKExternal) Delete(ctx context.Context, _ xpresource.Managed) error {
   686  	n.logger.Debug("Deleting the external resource")
   687  	if n.instanceDiff == nil {
   688  		n.instanceDiff = tf.NewInstanceDiff()
   689  	}
   690  
   691  	n.instanceDiff.Destroy = true
   692  	start := time.Now()
   693  	newState, diag := n.resourceSchema.Apply(ctx, n.opTracker.GetTfState(), n.instanceDiff, n.ts.Meta)
   694  	metrics.ExternalAPITime.WithLabelValues("delete").Observe(time.Since(start).Seconds())
   695  	if diag != nil && diag.HasError() {
   696  		return errors.Errorf("failed to delete the resource: %v", diag)
   697  	}
   698  	n.opTracker.SetTfState(newState)
   699  	// mark the resource as logically deleted if the TF call clears the state
   700  	n.opTracker.SetDeleted(newState == nil)
   701  	return nil
   702  }
   703  
   704  func (n *terraformPluginSDKExternal) fromInstanceStateToJSONMap(newState *tf.InstanceState) (map[string]interface{}, cty.Value, error) {
   705  	impliedType := n.config.TerraformResource.CoreConfigSchema().ImpliedType()
   706  	attrsAsCtyValue, err := newState.AttrsAsObjectValue(impliedType)
   707  	if err != nil {
   708  		return nil, cty.NilVal, errors.Wrap(err, "could not convert attrs to cty value")
   709  	}
   710  	stateValueMap, err := schema.StateValueToJSONMap(attrsAsCtyValue, impliedType)
   711  	if err != nil {
   712  		return nil, cty.NilVal, errors.Wrap(err, "could not convert instance state value to JSON")
   713  	}
   714  	return stateValueMap, attrsAsCtyValue, nil
   715  }