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  }