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  }