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