github.com/crossplane/upjet@v1.3.0/pkg/controller/nofork_store.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  	"sync"
     9  	"sync/atomic"
    10  
    11  	"github.com/crossplane/crossplane-runtime/pkg/logging"
    12  	xpresource "github.com/crossplane/crossplane-runtime/pkg/resource"
    13  	"github.com/hashicorp/terraform-plugin-go/tfprotov5"
    14  	tfsdk "github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
    15  	"k8s.io/apimachinery/pkg/types"
    16  
    17  	"github.com/crossplane/upjet/pkg/resource"
    18  	"github.com/crossplane/upjet/pkg/terraform"
    19  )
    20  
    21  // AsyncTracker holds information for a managed resource to track the
    22  // async Terraform operations and the
    23  // Terraform state (TF SDKv2 or TF Plugin Framework) of the external resource
    24  //
    25  // The typical usage is to instantiate an AsyncTracker for a managed resource,
    26  // and store in a global OperationTrackerStore, to carry information between
    27  // reconciliation scopes.
    28  //
    29  // When an asynchronous Terraform operation is started for the resource
    30  // in a reconciliation (e.g. with a goroutine), consumers can mark an operation start
    31  // on the LastOperation field, then access the operation status in the
    32  // forthcoming reconciliation cycles, and act upon
    33  // (e.g. hold further actions if there is an ongoing operation, mark the end
    34  // when underlying Terraform operation is completed, save the resulting
    35  // terraform state etc.)
    36  //
    37  // When utilized without the LastOperation usage, it can act as a Terraform
    38  // state cache for synchronous reconciliations
    39  type AsyncTracker struct {
    40  	// LastOperation holds information about the most recent operation.
    41  	// Consumers are responsible for managing the last operation by starting,
    42  	// ending and flushing it when done with processing the results.
    43  	// Designed to allow only one ongoing operation at a given time.
    44  	LastOperation *terraform.Operation
    45  	logger        logging.Logger
    46  	mu            *sync.Mutex
    47  	// TF Plugin SDKv2 instance state for TF Plugin SDKv2-based resources
    48  	tfState *tfsdk.InstanceState
    49  	// TF Plugin Framework instance state for TF Plugin Framework-based resources
    50  	fwState *tfprotov5.DynamicValue
    51  	// lifecycle of certain external resources are bound to a parent resource's
    52  	// lifecycle, and they cannot be deleted without actually deleting
    53  	// the owning external resource (e.g.,  a database resource as the parent
    54  	// resource and a database configuration resource whose lifecycle is bound
    55  	// to it. For such resources, Terraform still removes the state for them
    56  	// after a successful delete call either by resetting to some defaults in
    57  	// the parent resource, or by a noop. We logically delete such resources as
    58  	// deleted after a successful delete call so that the next observe can
    59  	// tell the managed reconciler that the resource no longer "exists".
    60  	isDeleted atomic.Bool
    61  }
    62  
    63  type AsyncTrackerOption func(manager *AsyncTracker)
    64  
    65  // WithAsyncTrackerLogger sets the logger of AsyncTracker.
    66  func WithAsyncTrackerLogger(l logging.Logger) AsyncTrackerOption {
    67  	return func(w *AsyncTracker) {
    68  		w.logger = l
    69  	}
    70  }
    71  
    72  // NewAsyncTracker initializes an AsyncTracker with given options
    73  func NewAsyncTracker(opts ...AsyncTrackerOption) *AsyncTracker {
    74  	w := &AsyncTracker{
    75  		LastOperation: &terraform.Operation{},
    76  		logger:        logging.NewNopLogger(),
    77  		mu:            &sync.Mutex{},
    78  	}
    79  	for _, f := range opts {
    80  		f(w)
    81  	}
    82  	return w
    83  }
    84  
    85  // GetTfState returns the stored Terraform Plugin SDKv2 InstanceState for
    86  // SDKv2 Terraform resources
    87  // MUST be only used for SDKv2 resources.
    88  func (a *AsyncTracker) GetTfState() *tfsdk.InstanceState {
    89  	a.mu.Lock()
    90  	defer a.mu.Unlock()
    91  	return a.tfState
    92  }
    93  
    94  // HasState returns whether the AsyncTracker has a SDKv2 state stored.
    95  // MUST be only used for SDKv2 resources.
    96  func (a *AsyncTracker) HasState() bool {
    97  	a.mu.Lock()
    98  	defer a.mu.Unlock()
    99  	return a.tfState != nil && a.tfState.ID != ""
   100  }
   101  
   102  // SetTfState stores the given SDKv2 Terraform InstanceState into
   103  // the AsyncTracker
   104  // MUST be only used for SDKv2 resources.
   105  func (a *AsyncTracker) SetTfState(state *tfsdk.InstanceState) {
   106  	a.mu.Lock()
   107  	defer a.mu.Unlock()
   108  	a.tfState = state
   109  }
   110  
   111  // GetTfID returns the Terraform ID of the external resource currently
   112  // stored in this AsyncTracker's SDKv2 instance state.
   113  // MUST be only used for SDKv2 resources.
   114  func (a *AsyncTracker) GetTfID() string {
   115  	a.mu.Lock()
   116  	defer a.mu.Unlock()
   117  	if a.tfState == nil {
   118  		return ""
   119  	}
   120  	return a.tfState.ID
   121  }
   122  
   123  // IsDeleted returns whether the associated external resource
   124  // has logically been deleted.
   125  func (a *AsyncTracker) IsDeleted() bool {
   126  	return a.isDeleted.Load()
   127  }
   128  
   129  // SetDeleted sets the logical deletion status of
   130  // the associated external resource.
   131  func (a *AsyncTracker) SetDeleted(deleted bool) {
   132  	a.isDeleted.Store(deleted)
   133  }
   134  
   135  // GetFrameworkTFState returns the stored Terraform Plugin Framework external
   136  // resource state in this AsyncTracker as *tfprotov5.DynamicValue
   137  // MUST be used only for Terraform Plugin Framework resources
   138  func (a *AsyncTracker) GetFrameworkTFState() *tfprotov5.DynamicValue {
   139  	a.mu.Lock()
   140  	defer a.mu.Unlock()
   141  	return a.fwState
   142  }
   143  
   144  // HasFrameworkTFState returns whether this AsyncTracker has a
   145  // Terraform Plugin Framework state stored.
   146  // MUST be used only for Terraform Plugin Framework resources
   147  func (a *AsyncTracker) HasFrameworkTFState() bool {
   148  	a.mu.Lock()
   149  	defer a.mu.Unlock()
   150  	return a.fwState != nil
   151  }
   152  
   153  // SetFrameworkTFState stores the given *tfprotov5.DynamicValue Terraform Plugin Framework external
   154  // resource state into this AsyncTracker's fwstate
   155  // MUST be used only for Terraform Plugin Framework resources
   156  func (a *AsyncTracker) SetFrameworkTFState(state *tfprotov5.DynamicValue) {
   157  	a.mu.Lock()
   158  	defer a.mu.Unlock()
   159  	a.fwState = state
   160  }
   161  
   162  // OperationTrackerStore stores the AsyncTracker instances associated with the
   163  // managed resource instance.
   164  type OperationTrackerStore struct {
   165  	store  map[types.UID]*AsyncTracker
   166  	logger logging.Logger
   167  	mu     *sync.Mutex
   168  }
   169  
   170  // NewOperationStore returns a new OperationTrackerStore instance
   171  func NewOperationStore(l logging.Logger) *OperationTrackerStore {
   172  	ops := &OperationTrackerStore{
   173  		store:  map[types.UID]*AsyncTracker{},
   174  		logger: l,
   175  		mu:     &sync.Mutex{},
   176  	}
   177  
   178  	return ops
   179  }
   180  
   181  // Tracker returns the associated *AsyncTracker stored in this
   182  // OperationTrackerStore for the given managed resource.
   183  // If there is no tracker stored previously, a new AsyncTracker is created and
   184  // stored for the specified managed resource. Subsequent calls with the same managed
   185  // resource will return the previously instantiated and stored AsyncTracker
   186  // for that managed resource
   187  func (ops *OperationTrackerStore) Tracker(tr resource.Terraformed) *AsyncTracker {
   188  	ops.mu.Lock()
   189  	defer ops.mu.Unlock()
   190  	tracker, ok := ops.store[tr.GetUID()]
   191  	if !ok {
   192  		l := ops.logger.WithValues("trackerUID", tr.GetUID(), "resourceName", tr.GetName(), "gvk", tr.GetObjectKind().GroupVersionKind().String())
   193  		ops.store[tr.GetUID()] = NewAsyncTracker(WithAsyncTrackerLogger(l))
   194  		tracker = ops.store[tr.GetUID()]
   195  	}
   196  	return tracker
   197  }
   198  
   199  // RemoveTracker will remove the stored AsyncTracker of the given managed
   200  // resource from this OperationTrackerStore.
   201  func (ops *OperationTrackerStore) RemoveTracker(obj xpresource.Object) error {
   202  	ops.mu.Lock()
   203  	defer ops.mu.Unlock()
   204  	delete(ops.store, obj.GetUID())
   205  	return nil
   206  }