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

     1  // SPDX-FileCopyrightText: 2024 The Crossplane Authors <https://crossplane.io>
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package controller
     6  
     7  import (
     8  	"context"
     9  	"encoding/json"
    10  	"fmt"
    11  	"math"
    12  	"math/big"
    13  	"strings"
    14  	"time"
    15  
    16  	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
    17  	"github.com/crossplane/crossplane-runtime/pkg/logging"
    18  	"github.com/crossplane/crossplane-runtime/pkg/meta"
    19  	"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
    20  	xpresource "github.com/crossplane/crossplane-runtime/pkg/resource"
    21  	fwdiag "github.com/hashicorp/terraform-plugin-framework/diag"
    22  	fwprovider "github.com/hashicorp/terraform-plugin-framework/provider"
    23  	"github.com/hashicorp/terraform-plugin-framework/providerserver"
    24  	fwresource "github.com/hashicorp/terraform-plugin-framework/resource"
    25  	rschema "github.com/hashicorp/terraform-plugin-framework/resource/schema"
    26  	"github.com/hashicorp/terraform-plugin-go/tfprotov5"
    27  	"github.com/hashicorp/terraform-plugin-go/tftypes"
    28  	"github.com/pkg/errors"
    29  	corev1 "k8s.io/api/core/v1"
    30  	"k8s.io/apimachinery/pkg/util/sets"
    31  	"sigs.k8s.io/controller-runtime/pkg/client"
    32  
    33  	"github.com/crossplane/upjet/pkg/config"
    34  	"github.com/crossplane/upjet/pkg/metrics"
    35  	"github.com/crossplane/upjet/pkg/resource"
    36  	upjson "github.com/crossplane/upjet/pkg/resource/json"
    37  	"github.com/crossplane/upjet/pkg/terraform"
    38  )
    39  
    40  // TerraformPluginFrameworkConnector is an external client, with credentials and
    41  // other configuration parameters, for Terraform Plugin Framework resources. You
    42  // can use NewTerraformPluginFrameworkConnector to construct.
    43  type TerraformPluginFrameworkConnector struct {
    44  	getTerraformSetup           terraform.SetupFn
    45  	kube                        client.Client
    46  	config                      *config.Resource
    47  	logger                      logging.Logger
    48  	metricRecorder              *metrics.MetricRecorder
    49  	operationTrackerStore       *OperationTrackerStore
    50  	isManagementPoliciesEnabled bool
    51  }
    52  
    53  // TerraformPluginFrameworkConnectorOption allows you to configure TerraformPluginFrameworkConnector.
    54  type TerraformPluginFrameworkConnectorOption func(connector *TerraformPluginFrameworkConnector)
    55  
    56  // WithTerraformPluginFrameworkLogger configures a logger for the TerraformPluginFrameworkConnector.
    57  func WithTerraformPluginFrameworkLogger(l logging.Logger) TerraformPluginFrameworkConnectorOption {
    58  	return func(c *TerraformPluginFrameworkConnector) {
    59  		c.logger = l
    60  	}
    61  }
    62  
    63  // WithTerraformPluginFrameworkMetricRecorder configures a metrics.MetricRecorder for the
    64  // TerraformPluginFrameworkConnectorOption.
    65  func WithTerraformPluginFrameworkMetricRecorder(r *metrics.MetricRecorder) TerraformPluginFrameworkConnectorOption {
    66  	return func(c *TerraformPluginFrameworkConnector) {
    67  		c.metricRecorder = r
    68  	}
    69  }
    70  
    71  // WithTerraformPluginFrameworkManagementPolicies configures whether the client should
    72  // handle management policies.
    73  func WithTerraformPluginFrameworkManagementPolicies(isManagementPoliciesEnabled bool) TerraformPluginFrameworkConnectorOption {
    74  	return func(c *TerraformPluginFrameworkConnector) {
    75  		c.isManagementPoliciesEnabled = isManagementPoliciesEnabled
    76  	}
    77  }
    78  
    79  // NewTerraformPluginFrameworkConnector creates a new
    80  // TerraformPluginFrameworkConnector with given options.
    81  func NewTerraformPluginFrameworkConnector(kube client.Client, sf terraform.SetupFn, cfg *config.Resource, ots *OperationTrackerStore, opts ...TerraformPluginFrameworkConnectorOption) *TerraformPluginFrameworkConnector {
    82  	connector := &TerraformPluginFrameworkConnector{
    83  		getTerraformSetup:     sf,
    84  		kube:                  kube,
    85  		config:                cfg,
    86  		operationTrackerStore: ots,
    87  	}
    88  	for _, f := range opts {
    89  		f(connector)
    90  	}
    91  	return connector
    92  }
    93  
    94  type terraformPluginFrameworkExternalClient struct {
    95  	ts             terraform.Setup
    96  	config         *config.Resource
    97  	logger         logging.Logger
    98  	metricRecorder *metrics.MetricRecorder
    99  	opTracker      *AsyncTracker
   100  	resource       fwresource.Resource
   101  	server         tfprotov5.ProviderServer
   102  	params         map[string]any
   103  	planResponse   *tfprotov5.PlanResourceChangeResponse
   104  	resourceSchema rschema.Schema
   105  	// the terraform value type associated with the resource schema
   106  	resourceValueTerraformType tftypes.Type
   107  }
   108  
   109  // Connect makes sure the underlying client is ready to issue requests to the
   110  // provider API.
   111  func (c *TerraformPluginFrameworkConnector) Connect(ctx context.Context, mg xpresource.Managed) (managed.ExternalClient, error) { //nolint:gocyclo
   112  	c.metricRecorder.ObserveReconcileDelay(mg.GetObjectKind().GroupVersionKind(), mg.GetName())
   113  	logger := c.logger.WithValues("uid", mg.GetUID(), "name", mg.GetName(), "gvk", mg.GetObjectKind().GroupVersionKind().String())
   114  	logger.Debug("Connecting to the service provider")
   115  	start := time.Now()
   116  	ts, err := c.getTerraformSetup(ctx, c.kube, mg)
   117  	metrics.ExternalAPITime.WithLabelValues("connect").Observe(time.Since(start).Seconds())
   118  	if err != nil {
   119  		return nil, errors.Wrap(err, errGetTerraformSetup)
   120  	}
   121  
   122  	tr := mg.(resource.Terraformed)
   123  	opTracker := c.operationTrackerStore.Tracker(tr)
   124  	externalName := meta.GetExternalName(tr)
   125  	params, err := getExtendedParameters(ctx, tr, externalName, c.config, ts, c.isManagementPoliciesEnabled, c.kube)
   126  	if err != nil {
   127  		return nil, errors.Wrapf(err, "failed to get the extended parameters for resource %q", mg.GetName())
   128  	}
   129  
   130  	resourceSchema, err := c.getResourceSchema(ctx)
   131  	if err != nil {
   132  		return nil, errors.Wrap(err, "could not retrieve resource schema")
   133  	}
   134  	resourceTfValueType := resourceSchema.Type().TerraformType(ctx)
   135  	hasState := false
   136  	if opTracker.HasFrameworkTFState() {
   137  		tfStateValue, err := opTracker.GetFrameworkTFState().Unmarshal(resourceTfValueType)
   138  		if err != nil {
   139  			return nil, errors.Wrap(err, "cannot unmarshal TF state dynamic value during state existence check")
   140  		}
   141  		hasState = err == nil && !tfStateValue.IsNull()
   142  	}
   143  
   144  	if !hasState {
   145  		logger.Debug("Instance state not found in cache, reconstructing...")
   146  		tfState, err := tr.GetObservation()
   147  		if err != nil {
   148  			return nil, errors.Wrap(err, "failed to get the observation")
   149  		}
   150  		copyParams := len(tfState) == 0
   151  		if err = resource.GetSensitiveParameters(ctx, &APISecretClient{kube: c.kube}, tr, tfState, tr.GetConnectionDetailsMapping()); err != nil {
   152  			return nil, errors.Wrap(err, "cannot store sensitive parameters into tfState")
   153  		}
   154  		c.config.ExternalName.SetIdentifierArgumentFn(tfState, externalName)
   155  		tfState["id"] = params["id"]
   156  		if copyParams {
   157  			tfState = copyParameters(tfState, params)
   158  		}
   159  
   160  		tfStateDynamicValue, err := protov5DynamicValueFromMap(tfState, resourceTfValueType)
   161  		if err != nil {
   162  			return nil, errors.Wrap(err, "cannot construct dynamic value for TF state")
   163  		}
   164  		opTracker.SetFrameworkTFState(tfStateDynamicValue)
   165  	}
   166  
   167  	configuredProviderServer, err := c.configureProvider(ctx, ts)
   168  	if err != nil {
   169  		return nil, errors.Wrap(err, "could not configure provider server")
   170  	}
   171  
   172  	return &terraformPluginFrameworkExternalClient{
   173  		ts:                         ts,
   174  		config:                     c.config,
   175  		logger:                     logger,
   176  		metricRecorder:             c.metricRecorder,
   177  		opTracker:                  opTracker,
   178  		resource:                   c.config.TerraformPluginFrameworkResource,
   179  		server:                     configuredProviderServer,
   180  		params:                     params,
   181  		resourceSchema:             resourceSchema,
   182  		resourceValueTerraformType: resourceTfValueType,
   183  	}, nil
   184  }
   185  
   186  // getResourceSchema returns the Terraform Plugin Framework-style resource schema for the configured framework resource on the connector
   187  func (c *TerraformPluginFrameworkConnector) getResourceSchema(ctx context.Context) (rschema.Schema, error) {
   188  	res := c.config.TerraformPluginFrameworkResource
   189  	schemaResp := &fwresource.SchemaResponse{}
   190  	res.Schema(ctx, fwresource.SchemaRequest{}, schemaResp)
   191  	if schemaResp.Diagnostics.HasError() {
   192  		fwErrors := frameworkDiagnosticsToString(schemaResp.Diagnostics)
   193  		return rschema.Schema{}, errors.Errorf("could not retrieve resource schema: %s", fwErrors)
   194  	}
   195  
   196  	return schemaResp.Schema, nil
   197  }
   198  
   199  // configureProvider returns a configured Terraform protocol v5 provider server
   200  // with the preconfigured provider instance in the terraform setup.
   201  // The provider instance used should be already preconfigured
   202  // at the terraform setup layer with the relevant provider meta if needed
   203  // by the provider implementation.
   204  func (c *TerraformPluginFrameworkConnector) configureProvider(ctx context.Context, ts terraform.Setup) (tfprotov5.ProviderServer, error) {
   205  	var schemaResp fwprovider.SchemaResponse
   206  	ts.FrameworkProvider.Schema(ctx, fwprovider.SchemaRequest{}, &schemaResp)
   207  	if schemaResp.Diagnostics.HasError() {
   208  		fwDiags := frameworkDiagnosticsToString(schemaResp.Diagnostics)
   209  		return nil, fmt.Errorf("cannot retrieve provider schema: %s", fwDiags)
   210  	}
   211  	providerServer := providerserver.NewProtocol5(ts.FrameworkProvider)()
   212  
   213  	providerConfigDynamicVal, err := protov5DynamicValueFromMap(ts.Configuration, schemaResp.Schema.Type().TerraformType(ctx))
   214  	if err != nil {
   215  		return nil, errors.Wrap(err, "cannot construct dynamic value for TF provider config")
   216  	}
   217  
   218  	configureProviderReq := &tfprotov5.ConfigureProviderRequest{
   219  		TerraformVersion: "crossTF000",
   220  		Config:           providerConfigDynamicVal,
   221  	}
   222  	providerResp, err := providerServer.ConfigureProvider(ctx, configureProviderReq)
   223  	if err != nil {
   224  		return nil, errors.Wrap(err, "cannot configure framework provider")
   225  	}
   226  	if fatalDiags := getFatalDiagnostics(providerResp.Diagnostics); fatalDiags != nil {
   227  		return nil, errors.Wrap(fatalDiags, "provider configure request failed")
   228  	}
   229  	return providerServer, nil
   230  }
   231  
   232  // getDiffPlanResponse calls the underlying native TF provider's PlanResourceChange RPC,
   233  // and returns the planned state and whether a diff exists.
   234  // If plan response contains non-empty RequiresReplace (i.e. the resource needs
   235  // to be recreated) an error is returned as Crossplane Resource Model (XRM)
   236  // prohibits resource re-creations and rejects this plan.
   237  func (n *terraformPluginFrameworkExternalClient) getDiffPlanResponse(ctx context.Context,
   238  	tfStateValue tftypes.Value) (*tfprotov5.PlanResourceChangeResponse, bool, error) {
   239  	tfConfigDynamicVal, err := protov5DynamicValueFromMap(n.params, n.resourceValueTerraformType)
   240  	if err != nil {
   241  		return nil, false, errors.Wrap(err, "cannot construct dynamic value for TF Config")
   242  	}
   243  
   244  	//
   245  	tfPlannedStateDynamicVal, err := protov5DynamicValueFromMap(n.params, n.resourceValueTerraformType)
   246  	if err != nil {
   247  		return nil, false, errors.Wrap(err, "cannot construct dynamic value for TF Planned State")
   248  	}
   249  
   250  	prcReq := &tfprotov5.PlanResourceChangeRequest{
   251  		TypeName:         n.config.Name,
   252  		PriorState:       n.opTracker.GetFrameworkTFState(),
   253  		Config:           tfConfigDynamicVal,
   254  		ProposedNewState: tfPlannedStateDynamicVal,
   255  	}
   256  	planResponse, err := n.server.PlanResourceChange(ctx, prcReq)
   257  	if err != nil {
   258  		return nil, false, errors.Wrap(err, "cannot plan change")
   259  	}
   260  	if fatalDiags := getFatalDiagnostics(planResponse.Diagnostics); fatalDiags != nil {
   261  		return nil, false, errors.Wrap(fatalDiags, "plan resource change request failed")
   262  	}
   263  
   264  	plannedStateValue, err := planResponse.PlannedState.Unmarshal(n.resourceValueTerraformType)
   265  	if err != nil {
   266  		return nil, false, errors.Wrap(err, "cannot unmarshal planned state")
   267  	}
   268  
   269  	rawDiff, err := plannedStateValue.Diff(tfStateValue)
   270  	if err != nil {
   271  		return nil, false, errors.Wrap(err, "cannot compare prior state and plan")
   272  	}
   273  
   274  	// Filter diffs that have unknown plan values, which correspond to
   275  	// computed fields, and null plan values, which correspond to
   276  	// not-specified fields. Such cases cause unnecessary diff detection
   277  	// when only computed attributes or not-specified argument diffs
   278  	// exist in the raw diff and no actual diff exists in the
   279  	// parametrizable attributes.
   280  	filteredDiff := make([]tftypes.ValueDiff, 0)
   281  	for _, diff := range rawDiff {
   282  		if diff.Value1.IsKnown() && !diff.Value1.IsNull() {
   283  			filteredDiff = append(filteredDiff, diff)
   284  		}
   285  	}
   286  
   287  	return planResponse, len(filteredDiff) > 0, nil
   288  }
   289  
   290  func (n *terraformPluginFrameworkExternalClient) Observe(ctx context.Context, mg xpresource.Managed) (managed.ExternalObservation, error) { //nolint:gocyclo
   291  	n.logger.Debug("Observing the external resource")
   292  
   293  	if meta.WasDeleted(mg) && n.opTracker.IsDeleted() {
   294  		return managed.ExternalObservation{
   295  			ResourceExists: false,
   296  		}, nil
   297  	}
   298  
   299  	readRequest := &tfprotov5.ReadResourceRequest{
   300  		TypeName:     n.config.Name,
   301  		CurrentState: n.opTracker.GetFrameworkTFState(),
   302  	}
   303  	readResponse, err := n.server.ReadResource(ctx, readRequest)
   304  
   305  	if err != nil {
   306  		return managed.ExternalObservation{}, errors.Wrap(err, "cannot read resource")
   307  	}
   308  
   309  	if fatalDiags := getFatalDiagnostics(readResponse.Diagnostics); fatalDiags != nil {
   310  		return managed.ExternalObservation{}, errors.Wrap(fatalDiags, "read resource request failed")
   311  	}
   312  
   313  	tfStateValue, err := readResponse.NewState.Unmarshal(n.resourceValueTerraformType)
   314  	if err != nil {
   315  		return managed.ExternalObservation{}, errors.Wrap(err, "cannot unmarshal state value")
   316  	}
   317  
   318  	n.opTracker.SetFrameworkTFState(readResponse.NewState)
   319  	resourceExists := !tfStateValue.IsNull()
   320  
   321  	var stateValueMap map[string]any
   322  	if resourceExists {
   323  		if conv, err := tfValueToGoValue(tfStateValue); err != nil {
   324  			return managed.ExternalObservation{}, errors.Wrap(err, "cannot convert instance state to JSON map")
   325  		} else {
   326  			stateValueMap = conv.(map[string]any)
   327  		}
   328  	}
   329  
   330  	planResponse, hasDiff, err := n.getDiffPlanResponse(ctx, tfStateValue)
   331  	if err != nil {
   332  		return managed.ExternalObservation{}, errors.Wrap(err, "cannot calculate diff")
   333  	}
   334  
   335  	n.planResponse = planResponse
   336  
   337  	var connDetails managed.ConnectionDetails
   338  	if !resourceExists && mg.GetDeletionTimestamp() != nil {
   339  		gvk := mg.GetObjectKind().GroupVersionKind()
   340  		metrics.DeletionTime.WithLabelValues(gvk.Group, gvk.Version, gvk.Kind).Observe(time.Since(mg.GetDeletionTimestamp().Time).Seconds())
   341  	}
   342  
   343  	specUpdateRequired := false
   344  	if resourceExists {
   345  		if mg.GetCondition(xpv1.TypeReady).Status == corev1.ConditionUnknown ||
   346  			mg.GetCondition(xpv1.TypeReady).Status == corev1.ConditionFalse {
   347  			addTTR(mg)
   348  		}
   349  		mg.SetConditions(xpv1.Available())
   350  		buff, err := upjson.TFParser.Marshal(stateValueMap)
   351  		if err != nil {
   352  			return managed.ExternalObservation{}, errors.Wrap(err, "cannot marshal the attributes of the new state for late-initialization")
   353  		}
   354  
   355  		policySet := sets.New[xpv1.ManagementAction](mg.(resource.Terraformed).GetManagementPolicies()...)
   356  		policyHasLateInit := policySet.HasAny(xpv1.ManagementActionLateInitialize, xpv1.ManagementActionAll)
   357  		if policyHasLateInit {
   358  			specUpdateRequired, err = mg.(resource.Terraformed).LateInitialize(buff)
   359  			if err != nil {
   360  				return managed.ExternalObservation{}, errors.Wrap(err, "cannot late-initialize the managed resource")
   361  			}
   362  		}
   363  
   364  		err = mg.(resource.Terraformed).SetObservation(stateValueMap)
   365  		if err != nil {
   366  			return managed.ExternalObservation{}, errors.Errorf("could not set observation: %v", err)
   367  		}
   368  		connDetails, err = resource.GetConnectionDetails(stateValueMap, mg.(resource.Terraformed), n.config)
   369  		if err != nil {
   370  			return managed.ExternalObservation{}, errors.Wrap(err, "cannot get connection details")
   371  		}
   372  		if !hasDiff {
   373  			n.metricRecorder.SetReconcileTime(mg.GetName())
   374  		}
   375  		if !specUpdateRequired {
   376  			resource.SetUpToDateCondition(mg, !hasDiff)
   377  		}
   378  		if nameChanged, err := n.setExternalName(mg, stateValueMap); err != nil {
   379  			return managed.ExternalObservation{}, errors.Wrapf(err, "failed to set the external-name of the managed resource during observe")
   380  		} else {
   381  			specUpdateRequired = specUpdateRequired || nameChanged
   382  		}
   383  	}
   384  
   385  	return managed.ExternalObservation{
   386  		ResourceExists:          resourceExists,
   387  		ResourceUpToDate:        !hasDiff,
   388  		ConnectionDetails:       connDetails,
   389  		ResourceLateInitialized: specUpdateRequired,
   390  	}, nil
   391  }
   392  
   393  func (n *terraformPluginFrameworkExternalClient) Create(ctx context.Context, mg xpresource.Managed) (managed.ExternalCreation, error) {
   394  	n.logger.Debug("Creating the external resource")
   395  
   396  	tfConfigDynamicVal, err := protov5DynamicValueFromMap(n.params, n.resourceValueTerraformType)
   397  	if err != nil {
   398  		return managed.ExternalCreation{}, errors.Wrap(err, "cannot construct dynamic value for TF Config")
   399  	}
   400  
   401  	applyRequest := &tfprotov5.ApplyResourceChangeRequest{
   402  		TypeName:     n.config.Name,
   403  		PriorState:   n.opTracker.GetFrameworkTFState(),
   404  		PlannedState: n.planResponse.PlannedState,
   405  		Config:       tfConfigDynamicVal,
   406  	}
   407  	start := time.Now()
   408  	applyResponse, err := n.server.ApplyResourceChange(ctx, applyRequest)
   409  	if err != nil {
   410  		return managed.ExternalCreation{}, errors.Wrap(err, "cannot create resource")
   411  	}
   412  	metrics.ExternalAPITime.WithLabelValues("create").Observe(time.Since(start).Seconds())
   413  	if fatalDiags := getFatalDiagnostics(applyResponse.Diagnostics); fatalDiags != nil {
   414  		return managed.ExternalCreation{}, errors.Wrap(fatalDiags, "resource creation call returned error diags")
   415  	}
   416  
   417  	newStateAfterApplyVal, err := applyResponse.NewState.Unmarshal(n.resourceValueTerraformType)
   418  	if err != nil {
   419  		return managed.ExternalCreation{}, errors.Wrap(err, "cannot unmarshal planned state")
   420  	}
   421  
   422  	if newStateAfterApplyVal.IsNull() {
   423  		return managed.ExternalCreation{}, errors.New("new state is empty after creation")
   424  	}
   425  
   426  	var stateValueMap map[string]any
   427  	if goval, err := tfValueToGoValue(newStateAfterApplyVal); err != nil {
   428  		return managed.ExternalCreation{}, errors.New("cannot convert native state to go map")
   429  	} else {
   430  		stateValueMap = goval.(map[string]any)
   431  	}
   432  
   433  	n.opTracker.SetFrameworkTFState(applyResponse.NewState)
   434  
   435  	if _, err := n.setExternalName(mg, stateValueMap); err != nil {
   436  		return managed.ExternalCreation{}, errors.Wrapf(err, "failed to set the external-name of the managed resource during create")
   437  	}
   438  
   439  	err = mg.(resource.Terraformed).SetObservation(stateValueMap)
   440  	if err != nil {
   441  		return managed.ExternalCreation{}, errors.Errorf("could not set observation: %v", err)
   442  	}
   443  
   444  	conn, err := resource.GetConnectionDetails(stateValueMap, mg.(resource.Terraformed), n.config)
   445  	if err != nil {
   446  		return managed.ExternalCreation{}, errors.Wrap(err, "cannot get connection details")
   447  	}
   448  
   449  	return managed.ExternalCreation{ConnectionDetails: conn}, nil
   450  }
   451  
   452  func (n *terraformPluginFrameworkExternalClient) planRequiresReplace() (bool, string) {
   453  	if n.planResponse == nil || len(n.planResponse.RequiresReplace) == 0 {
   454  		return false, ""
   455  	}
   456  
   457  	var sb strings.Builder
   458  	sb.WriteString("diff contains fields that require resource replacement: ")
   459  	for _, attrPath := range n.planResponse.RequiresReplace {
   460  		sb.WriteString(attrPath.String())
   461  		sb.WriteString(", ")
   462  	}
   463  	return true, sb.String()
   464  
   465  }
   466  
   467  func (n *terraformPluginFrameworkExternalClient) Update(ctx context.Context, mg xpresource.Managed) (managed.ExternalUpdate, error) {
   468  	n.logger.Debug("Updating the external resource")
   469  	// refuse plans that require replace for XRM compliance
   470  	if isReplace, fields := n.planRequiresReplace(); isReplace {
   471  		return managed.ExternalUpdate{}, errors.Errorf("diff contains fields that require resource replacement: %s", fields)
   472  	}
   473  
   474  	tfConfigDynamicVal, err := protov5DynamicValueFromMap(n.params, n.resourceValueTerraformType)
   475  	if err != nil {
   476  		return managed.ExternalUpdate{}, errors.Wrap(err, "cannot construct dynamic value for TF Config")
   477  	}
   478  
   479  	applyRequest := &tfprotov5.ApplyResourceChangeRequest{
   480  		TypeName:     n.config.Name,
   481  		PriorState:   n.opTracker.GetFrameworkTFState(),
   482  		PlannedState: n.planResponse.PlannedState,
   483  		Config:       tfConfigDynamicVal,
   484  	}
   485  	start := time.Now()
   486  	applyResponse, err := n.server.ApplyResourceChange(ctx, applyRequest)
   487  	if err != nil {
   488  		return managed.ExternalUpdate{}, errors.Wrap(err, "cannot update resource")
   489  	}
   490  	metrics.ExternalAPITime.WithLabelValues("update").Observe(time.Since(start).Seconds())
   491  	if fatalDiags := getFatalDiagnostics(applyResponse.Diagnostics); fatalDiags != nil {
   492  		return managed.ExternalUpdate{}, errors.Wrap(fatalDiags, "resource update call returned error diags")
   493  	}
   494  	n.opTracker.SetFrameworkTFState(applyResponse.NewState)
   495  
   496  	newStateAfterApplyVal, err := applyResponse.NewState.Unmarshal(n.resourceValueTerraformType)
   497  	if err != nil {
   498  		return managed.ExternalUpdate{}, errors.Wrap(err, "cannot unmarshal updated state")
   499  	}
   500  
   501  	if newStateAfterApplyVal.IsNull() {
   502  		return managed.ExternalUpdate{}, errors.New("new state is empty after update")
   503  	}
   504  
   505  	var stateValueMap map[string]any
   506  	if goval, err := tfValueToGoValue(newStateAfterApplyVal); err != nil {
   507  		return managed.ExternalUpdate{}, errors.New("cannot convert native state to go map")
   508  	} else {
   509  		stateValueMap = goval.(map[string]any)
   510  	}
   511  
   512  	err = mg.(resource.Terraformed).SetObservation(stateValueMap)
   513  	if err != nil {
   514  		return managed.ExternalUpdate{}, errors.Errorf("could not set observation: %v", err)
   515  	}
   516  
   517  	return managed.ExternalUpdate{}, nil
   518  }
   519  
   520  func (n *terraformPluginFrameworkExternalClient) Delete(ctx context.Context, _ xpresource.Managed) error {
   521  	n.logger.Debug("Deleting the external resource")
   522  
   523  	tfConfigDynamicVal, err := protov5DynamicValueFromMap(n.params, n.resourceValueTerraformType)
   524  	if err != nil {
   525  		return errors.Wrap(err, "cannot construct dynamic value for TF Config")
   526  	}
   527  	// set an empty planned state, this corresponds to deleting
   528  	plannedState, err := tfprotov5.NewDynamicValue(n.resourceValueTerraformType, tftypes.NewValue(n.resourceValueTerraformType, nil))
   529  	if err != nil {
   530  		return errors.Wrap(err, "cannot set the planned state for deletion")
   531  	}
   532  
   533  	applyRequest := &tfprotov5.ApplyResourceChangeRequest{
   534  		TypeName:     n.config.Name,
   535  		PriorState:   n.opTracker.GetFrameworkTFState(),
   536  		PlannedState: &plannedState,
   537  		Config:       tfConfigDynamicVal,
   538  	}
   539  	start := time.Now()
   540  	applyResponse, err := n.server.ApplyResourceChange(ctx, applyRequest)
   541  	if err != nil {
   542  		return errors.Wrap(err, "cannot delete resource")
   543  	}
   544  	metrics.ExternalAPITime.WithLabelValues("delete").Observe(time.Since(start).Seconds())
   545  	if fatalDiags := getFatalDiagnostics(applyResponse.Diagnostics); fatalDiags != nil {
   546  		return errors.Wrap(fatalDiags, "resource deletion call returned error diags")
   547  	}
   548  	n.opTracker.SetFrameworkTFState(applyResponse.NewState)
   549  
   550  	newStateAfterApplyVal, err := applyResponse.NewState.Unmarshal(n.resourceValueTerraformType)
   551  	if err != nil {
   552  		return errors.Wrap(err, "cannot unmarshal state after deletion")
   553  	}
   554  	// mark the resource as logically deleted if the TF call clears the state
   555  	n.opTracker.SetDeleted(newStateAfterApplyVal.IsNull())
   556  
   557  	return nil
   558  }
   559  
   560  func (n *terraformPluginFrameworkExternalClient) setExternalName(mg xpresource.Managed, stateValueMap map[string]interface{}) (bool, error) {
   561  	id, ok := stateValueMap["id"]
   562  	if !ok || id.(string) == "" {
   563  		return false, nil
   564  	}
   565  	newName, err := n.config.ExternalName.GetExternalNameFn(stateValueMap)
   566  	if err != nil {
   567  		return false, errors.Wrapf(err, "failed to compute the external-name from the state map of the resource with the ID %s", id)
   568  	}
   569  	oldName := meta.GetExternalName(mg)
   570  	// we have to make sure the newly set external-name is recorded
   571  	meta.SetExternalName(mg, newName)
   572  	return oldName != newName, nil
   573  }
   574  
   575  // tfValueToGoValue converts a given tftypes.Value to Go-native any type.
   576  // Useful for converting terraform values of state to JSON or for setting
   577  // observations at the MR.
   578  // Nested values are recursively converted.
   579  // Supported conversions:
   580  // tftypes.Object, tftypes.Map => map[string]any
   581  // tftypes.Set, tftypes.List, tftypes.Tuple => []string
   582  // tftypes.Bool => bool
   583  // tftypes.Number => int64, float64
   584  // tftypes.String => string
   585  // tftypes.DynamicPseudoType => conversion not supported and returns an error
   586  func tfValueToGoValue(input tftypes.Value) (any, error) { //nolint:gocyclo
   587  	if !input.IsKnown() {
   588  		return nil, fmt.Errorf("cannot convert unknown value")
   589  	}
   590  	if input.IsNull() {
   591  		return nil, nil
   592  	}
   593  	valType := input.Type()
   594  	switch {
   595  	case valType.Is(tftypes.Object{}), valType.Is(tftypes.Map{}):
   596  		destInterim := make(map[string]tftypes.Value)
   597  		dest := make(map[string]any)
   598  		if err := input.As(&destInterim); err != nil {
   599  			return nil, err
   600  		}
   601  		for k, v := range destInterim {
   602  			res, err := tfValueToGoValue(v)
   603  			if err != nil {
   604  				return nil, err
   605  			}
   606  			dest[k] = res
   607  
   608  		}
   609  		return dest, nil
   610  	case valType.Is(tftypes.Set{}), valType.Is(tftypes.List{}), valType.Is(tftypes.Tuple{}):
   611  		destInterim := make([]tftypes.Value, 0)
   612  		if err := input.As(&destInterim); err != nil {
   613  			return nil, err
   614  		}
   615  		dest := make([]any, len(destInterim))
   616  		for i, v := range destInterim {
   617  			res, err := tfValueToGoValue(v)
   618  			if err != nil {
   619  				return nil, err
   620  			}
   621  			dest[i] = res
   622  		}
   623  		return dest, nil
   624  	case valType.Is(tftypes.Bool):
   625  		var x bool
   626  		return x, input.As(&x)
   627  	case valType.Is(tftypes.Number):
   628  		var valBigF big.Float
   629  		if err := input.As(&valBigF); err != nil {
   630  			return nil, err
   631  		}
   632  		// try to parse as integer
   633  		if valBigF.IsInt() {
   634  			intVal, accuracy := valBigF.Int64()
   635  			if accuracy != 0 {
   636  				return nil, fmt.Errorf("value %v cannot be represented as a 64-bit integer", valBigF)
   637  			}
   638  			return intVal, nil
   639  		}
   640  		// try to parse as float64
   641  		xf, accuracy := valBigF.Float64()
   642  		// Underflow
   643  		// Reference: https://pkg.go.dev/math/big#Float.Float64
   644  		if xf == 0 && accuracy != big.Exact {
   645  			return nil, fmt.Errorf("value %v cannot be represented as a 64-bit floating point", valBigF)
   646  		}
   647  
   648  		// Overflow
   649  		// Reference: https://pkg.go.dev/math/big#Float.Float64
   650  		if math.IsInf(xf, 0) {
   651  			return nil, fmt.Errorf("value %v cannot be represented as a 64-bit floating point", valBigF)
   652  		}
   653  		return xf, nil
   654  
   655  	case valType.Is(tftypes.String):
   656  		var x string
   657  		return x, input.As(&x)
   658  	case valType.Is(tftypes.DynamicPseudoType):
   659  		return nil, errors.New("DynamicPseudoType conversion is not supported")
   660  	default:
   661  		return nil, fmt.Errorf("input value has unknown type: %s", valType.String())
   662  	}
   663  }
   664  
   665  // getFatalDiagnostics traverses the given Terraform protov5 diagnostics type
   666  // and constructs a Go error. If the provided diag slice is empty, returns nil.
   667  func getFatalDiagnostics(diags []*tfprotov5.Diagnostic) error {
   668  	var errs error
   669  	var diagErrors []string
   670  	for _, tfdiag := range diags {
   671  		if tfdiag.Severity == tfprotov5.DiagnosticSeverityInvalid || tfdiag.Severity == tfprotov5.DiagnosticSeverityError {
   672  			diagErrors = append(diagErrors, fmt.Sprintf("%s: %s", tfdiag.Summary, tfdiag.Detail))
   673  		}
   674  	}
   675  	if len(diagErrors) > 0 {
   676  		errs = errors.New(strings.Join(diagErrors, "\n"))
   677  	}
   678  	return errs
   679  }
   680  
   681  // frameworkDiagnosticsToString constructs an error string from the provided
   682  // Plugin Framework diagnostics instance. Only Error severity diagnostics are
   683  // included.
   684  func frameworkDiagnosticsToString(fwdiags fwdiag.Diagnostics) string {
   685  	frameworkErrorDiags := fwdiags.Errors()
   686  	diagErrors := make([]string, 0, len(frameworkErrorDiags))
   687  	for _, tfdiag := range frameworkErrorDiags {
   688  		diagErrors = append(diagErrors, fmt.Sprintf("%s: %s", tfdiag.Summary(), tfdiag.Detail()))
   689  	}
   690  	return strings.Join(diagErrors, "\n")
   691  }
   692  
   693  // protov5DynamicValueFromMap constructs a protov5 DynamicValue given the
   694  // map[string]any using the terraform type as reference.
   695  func protov5DynamicValueFromMap(data map[string]any, terraformType tftypes.Type) (*tfprotov5.DynamicValue, error) {
   696  	jsonBytes, err := json.Marshal(data)
   697  	if err != nil {
   698  		return nil, errors.Wrap(err, "cannot marshal json")
   699  	}
   700  
   701  	tfValue, err := tftypes.ValueFromJSONWithOpts(jsonBytes, terraformType, tftypes.ValueFromJSONOpts{IgnoreUndefinedAttributes: true})
   702  	if err != nil {
   703  		return nil, errors.Wrap(err, "cannot construct tf value from json")
   704  	}
   705  
   706  	dynamicValue, err := tfprotov5.NewDynamicValue(terraformType, tfValue)
   707  	if err != nil {
   708  		return nil, errors.Wrap(err, "cannot construct dynamic value from tf value")
   709  	}
   710  
   711  	return &dynamicValue, nil
   712  }