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 }