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 }