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 }