github.com/crossplane/upjet@v1.3.0/pkg/controller/external_async_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 "time" 10 11 xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" 12 "github.com/crossplane/crossplane-runtime/pkg/logging" 13 "github.com/crossplane/crossplane-runtime/pkg/meta" 14 "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" 15 xpresource "github.com/crossplane/crossplane-runtime/pkg/resource" 16 "github.com/pkg/errors" 17 "sigs.k8s.io/controller-runtime/pkg/client" 18 19 "github.com/crossplane/upjet/pkg/config" 20 "github.com/crossplane/upjet/pkg/controller/handler" 21 "github.com/crossplane/upjet/pkg/metrics" 22 "github.com/crossplane/upjet/pkg/resource" 23 "github.com/crossplane/upjet/pkg/terraform" 24 tferrors "github.com/crossplane/upjet/pkg/terraform/errors" 25 ) 26 27 var defaultAsyncTimeout = 1 * time.Hour 28 29 // TerraformPluginSDKAsyncConnector is a managed reconciler Connecter 30 // implementation for reconciling Terraform plugin SDK v2 based 31 // resources. 32 type TerraformPluginSDKAsyncConnector struct { 33 *TerraformPluginSDKConnector 34 callback CallbackProvider 35 eventHandler *handler.EventHandler 36 } 37 38 // TerraformPluginSDKAsyncOption represents a configuration option for 39 // a TerraformPluginSDKAsyncConnector object. 40 type TerraformPluginSDKAsyncOption func(connector *TerraformPluginSDKAsyncConnector) 41 42 // NewTerraformPluginSDKAsyncConnector initializes a new 43 // TerraformPluginSDKAsyncConnector. 44 func NewTerraformPluginSDKAsyncConnector(kube client.Client, ots *OperationTrackerStore, sf terraform.SetupFn, cfg *config.Resource, opts ...TerraformPluginSDKAsyncOption) *TerraformPluginSDKAsyncConnector { 45 nfac := &TerraformPluginSDKAsyncConnector{ 46 TerraformPluginSDKConnector: NewTerraformPluginSDKConnector(kube, sf, cfg, ots), 47 } 48 for _, f := range opts { 49 f(nfac) 50 } 51 return nfac 52 } 53 54 func (c *TerraformPluginSDKAsyncConnector) Connect(ctx context.Context, mg xpresource.Managed) (managed.ExternalClient, error) { 55 ec, err := c.TerraformPluginSDKConnector.Connect(ctx, mg) 56 if err != nil { 57 return nil, errors.Wrap(err, "cannot initialize the Terraform plugin SDK async external client") 58 } 59 60 return &terraformPluginSDKAsyncExternal{ 61 terraformPluginSDKExternal: ec.(*terraformPluginSDKExternal), 62 callback: c.callback, 63 eventHandler: c.eventHandler, 64 }, nil 65 } 66 67 // WithTerraformPluginSDKAsyncConnectorEventHandler configures the 68 // EventHandler so that the Terraform plugin SDK external clients can requeue 69 // reconciliation requests. 70 func WithTerraformPluginSDKAsyncConnectorEventHandler(e *handler.EventHandler) TerraformPluginSDKAsyncOption { 71 return func(c *TerraformPluginSDKAsyncConnector) { 72 c.eventHandler = e 73 } 74 } 75 76 // WithTerraformPluginSDKAsyncCallbackProvider configures the controller to use 77 // async variant of the functions of the Terraform client and run given 78 // callbacks once those operations are completed. 79 func WithTerraformPluginSDKAsyncCallbackProvider(ac CallbackProvider) TerraformPluginSDKAsyncOption { 80 return func(c *TerraformPluginSDKAsyncConnector) { 81 c.callback = ac 82 } 83 } 84 85 // WithTerraformPluginSDKAsyncLogger configures a logger for the 86 // TerraformPluginSDKAsyncConnector. 87 func WithTerraformPluginSDKAsyncLogger(l logging.Logger) TerraformPluginSDKAsyncOption { 88 return func(c *TerraformPluginSDKAsyncConnector) { 89 c.logger = l 90 } 91 } 92 93 // WithTerraformPluginSDKAsyncMetricRecorder configures a 94 // metrics.MetricRecorder for the TerraformPluginSDKAsyncConnector. 95 func WithTerraformPluginSDKAsyncMetricRecorder(r *metrics.MetricRecorder) TerraformPluginSDKAsyncOption { 96 return func(c *TerraformPluginSDKAsyncConnector) { 97 c.metricRecorder = r 98 } 99 } 100 101 // WithTerraformPluginSDKAsyncManagementPolicies configures whether the client 102 // should handle management policies. 103 func WithTerraformPluginSDKAsyncManagementPolicies(isManagementPoliciesEnabled bool) TerraformPluginSDKAsyncOption { 104 return func(c *TerraformPluginSDKAsyncConnector) { 105 c.isManagementPoliciesEnabled = isManagementPoliciesEnabled 106 } 107 } 108 109 type terraformPluginSDKAsyncExternal struct { 110 *terraformPluginSDKExternal 111 callback CallbackProvider 112 eventHandler *handler.EventHandler 113 } 114 115 type CallbackFn func(error, context.Context) error 116 117 func (n *terraformPluginSDKAsyncExternal) Observe(ctx context.Context, mg xpresource.Managed) (managed.ExternalObservation, error) { 118 if n.opTracker.LastOperation.IsRunning() { 119 n.logger.WithValues("opType", n.opTracker.LastOperation.Type).Debug("ongoing async operation") 120 return managed.ExternalObservation{ 121 ResourceExists: true, 122 ResourceUpToDate: true, 123 }, nil 124 } 125 n.opTracker.LastOperation.Clear(true) 126 127 o, err := n.terraformPluginSDKExternal.Observe(ctx, mg) 128 // clear any previously reported LastAsyncOperation error condition here, 129 // because there are no pending updates on the existing resource and it's 130 // not scheduled to be deleted. 131 if err == nil && o.ResourceExists && o.ResourceUpToDate && !meta.WasDeleted(mg) { 132 mg.(resource.Terraformed).SetConditions(resource.LastAsyncOperationCondition(nil)) 133 mg.(resource.Terraformed).SetConditions(xpv1.ReconcileSuccess()) 134 n.opTracker.LastOperation.Clear(false) 135 } 136 return o, err 137 } 138 139 func (n *terraformPluginSDKAsyncExternal) Create(_ context.Context, mg xpresource.Managed) (managed.ExternalCreation, error) { 140 if !n.opTracker.LastOperation.MarkStart("create") { 141 return managed.ExternalCreation{}, errors.Errorf("%s operation that started at %s is still running", n.opTracker.LastOperation.Type, n.opTracker.LastOperation.StartTime().String()) 142 } 143 144 ctx, cancel := context.WithDeadline(context.Background(), n.opTracker.LastOperation.StartTime().Add(defaultAsyncTimeout)) 145 go func() { 146 defer cancel() 147 148 n.opTracker.logger.Debug("Async create starting...", "tfID", n.opTracker.GetTfID()) 149 _, err := n.terraformPluginSDKExternal.Create(ctx, mg) 150 err = tferrors.NewAsyncCreateFailed(err) 151 n.opTracker.LastOperation.SetError(err) 152 n.opTracker.logger.Debug("Async create ended.", "error", err, "tfID", n.opTracker.GetTfID()) 153 154 n.opTracker.LastOperation.MarkEnd() 155 if cErr := n.callback.Create(mg.GetName())(err, ctx); cErr != nil { 156 n.opTracker.logger.Info("Async create callback failed", "error", cErr.Error()) 157 } 158 }() 159 160 return managed.ExternalCreation{}, n.opTracker.LastOperation.Error() 161 } 162 163 func (n *terraformPluginSDKAsyncExternal) Update(_ context.Context, mg xpresource.Managed) (managed.ExternalUpdate, error) { 164 if !n.opTracker.LastOperation.MarkStart("update") { 165 return managed.ExternalUpdate{}, errors.Errorf("%s operation that started at %s is still running", n.opTracker.LastOperation.Type, n.opTracker.LastOperation.StartTime().String()) 166 } 167 168 ctx, cancel := context.WithDeadline(context.Background(), n.opTracker.LastOperation.StartTime().Add(defaultAsyncTimeout)) 169 go func() { 170 defer cancel() 171 172 n.opTracker.logger.Debug("Async update starting...", "tfID", n.opTracker.GetTfID()) 173 _, err := n.terraformPluginSDKExternal.Update(ctx, mg) 174 err = tferrors.NewAsyncUpdateFailed(err) 175 n.opTracker.LastOperation.SetError(err) 176 n.opTracker.logger.Debug("Async update ended.", "error", err, "tfID", n.opTracker.GetTfID()) 177 178 n.opTracker.LastOperation.MarkEnd() 179 if cErr := n.callback.Update(mg.GetName())(err, ctx); cErr != nil { 180 n.opTracker.logger.Info("Async update callback failed", "error", cErr.Error()) 181 } 182 }() 183 184 return managed.ExternalUpdate{}, n.opTracker.LastOperation.Error() 185 } 186 187 func (n *terraformPluginSDKAsyncExternal) Delete(_ context.Context, mg xpresource.Managed) error { 188 switch { 189 case n.opTracker.LastOperation.Type == "delete": 190 n.opTracker.logger.Debug("The previous delete operation is still ongoing", "tfID", n.opTracker.GetTfID()) 191 return nil 192 case !n.opTracker.LastOperation.MarkStart("delete"): 193 return errors.Errorf("%s operation that started at %s is still running", n.opTracker.LastOperation.Type, n.opTracker.LastOperation.StartTime().String()) 194 } 195 196 ctx, cancel := context.WithDeadline(context.Background(), n.opTracker.LastOperation.StartTime().Add(defaultAsyncTimeout)) 197 go func() { 198 defer cancel() 199 200 n.opTracker.logger.Debug("Async delete starting...", "tfID", n.opTracker.GetTfID()) 201 err := tferrors.NewAsyncDeleteFailed(n.terraformPluginSDKExternal.Delete(ctx, mg)) 202 n.opTracker.LastOperation.SetError(err) 203 n.opTracker.logger.Debug("Async delete ended.", "error", err, "tfID", n.opTracker.GetTfID()) 204 205 n.opTracker.LastOperation.MarkEnd() 206 if cErr := n.callback.Destroy(mg.GetName())(err, ctx); cErr != nil { 207 n.opTracker.logger.Info("Async delete callback failed", "error", cErr.Error()) 208 } 209 }() 210 211 return n.opTracker.LastOperation.Error() 212 }