github.com/crossplane/upjet@v1.3.0/pkg/controller/api.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  	xpresource "github.com/crossplane/crossplane-runtime/pkg/resource"
    12  	"github.com/pkg/errors"
    13  	v1 "k8s.io/api/core/v1"
    14  	"k8s.io/apimachinery/pkg/runtime/schema"
    15  	"k8s.io/apimachinery/pkg/types"
    16  	"sigs.k8s.io/controller-runtime/pkg/client"
    17  	ctrl "sigs.k8s.io/controller-runtime/pkg/manager"
    18  
    19  	"github.com/crossplane/upjet/pkg/controller/handler"
    20  	"github.com/crossplane/upjet/pkg/resource"
    21  	"github.com/crossplane/upjet/pkg/terraform"
    22  )
    23  
    24  const (
    25  	errGetFmt              = "cannot get resource %s/%s after an async %s"
    26  	errUpdateStatusFmt     = "cannot update status of the resource %s/%s after an async %s"
    27  	errReconcileRequestFmt = "cannot request the reconciliation of the resource %s/%s after an async %s"
    28  )
    29  
    30  // crossplane-runtime error constants
    31  const (
    32  	errXPReconcileCreate = "create failed"
    33  	errXPReconcileUpdate = "update failed"
    34  	errXPReconcileDelete = "delete failed"
    35  )
    36  
    37  const (
    38  	rateLimiterCallback = "asyncCallback"
    39  )
    40  
    41  var _ CallbackProvider = &APICallbacks{}
    42  
    43  // APISecretClient is a client for getting k8s secrets
    44  type APISecretClient struct {
    45  	kube client.Client
    46  }
    47  
    48  // GetSecretData gets and returns data for the referenced secret
    49  func (a *APISecretClient) GetSecretData(ctx context.Context, ref *xpv1.SecretReference) (map[string][]byte, error) {
    50  	secret := &v1.Secret{}
    51  	if err := a.kube.Get(ctx, types.NamespacedName{Namespace: ref.Namespace, Name: ref.Name}, secret); err != nil {
    52  		return nil, err
    53  	}
    54  	return secret.Data, nil
    55  }
    56  
    57  // GetSecretValue gets and returns value for key of the referenced secret
    58  func (a *APISecretClient) GetSecretValue(ctx context.Context, sel xpv1.SecretKeySelector) ([]byte, error) {
    59  	d, err := a.GetSecretData(ctx, &sel.SecretReference)
    60  	if err != nil {
    61  		return nil, errors.Wrap(err, "cannot get secret data")
    62  	}
    63  	return d[sel.Key], err
    64  }
    65  
    66  // APICallbacksOption represents a configurable option for the APICallbacks
    67  type APICallbacksOption func(callbacks *APICallbacks)
    68  
    69  // WithEventHandler sets the EventHandler for the APICallbacks so that
    70  // the APICallbacks instance can requeue reconcile requests in the
    71  // context of the asynchronous operations.
    72  func WithEventHandler(e *handler.EventHandler) APICallbacksOption {
    73  	return func(callbacks *APICallbacks) {
    74  		callbacks.eventHandler = e
    75  	}
    76  }
    77  
    78  // WithStatusUpdates sets whether the LastAsyncOperation status condition
    79  // is enabled. If set to false, APICallbacks will not use the
    80  // LastAsyncOperation status condition for reporting ongoing async
    81  // operations or errors. Error conditions will still be reported
    82  // as usual in the `Synced` status condition.
    83  func WithStatusUpdates(enabled bool) APICallbacksOption {
    84  	return func(callbacks *APICallbacks) {
    85  		callbacks.enableStatusUpdates = enabled
    86  	}
    87  }
    88  
    89  // NewAPICallbacks returns a new APICallbacks.
    90  func NewAPICallbacks(m ctrl.Manager, of xpresource.ManagedKind, opts ...APICallbacksOption) *APICallbacks {
    91  	nt := func() resource.Terraformed {
    92  		return xpresource.MustCreateObject(schema.GroupVersionKind(of), m.GetScheme()).(resource.Terraformed)
    93  	}
    94  	cb := &APICallbacks{
    95  		kube:           m.GetClient(),
    96  		newTerraformed: nt,
    97  		// the default behavior is to use the LastAsyncOperation
    98  		// status condition for backwards compatibility.
    99  		enableStatusUpdates: true,
   100  	}
   101  	for _, o := range opts {
   102  		o(cb)
   103  	}
   104  	return cb
   105  }
   106  
   107  // APICallbacks providers callbacks that work on API resources.
   108  type APICallbacks struct {
   109  	eventHandler *handler.EventHandler
   110  
   111  	kube                client.Client
   112  	newTerraformed      func() resource.Terraformed
   113  	enableStatusUpdates bool
   114  }
   115  
   116  func (ac *APICallbacks) callbackFn(name, op string) terraform.CallbackFn {
   117  	return func(err error, ctx context.Context) error {
   118  		nn := types.NamespacedName{Name: name}
   119  		tr := ac.newTerraformed()
   120  		if kErr := ac.kube.Get(ctx, nn, tr); kErr != nil {
   121  			return errors.Wrapf(kErr, errGetFmt, tr.GetObjectKind().GroupVersionKind().String(), name, op)
   122  		}
   123  		// For the no-fork architecture, we will need to be able to report
   124  		// reconciliation errors. The proper place is the `Synced`
   125  		// status condition but we need changes in the managed reconciler
   126  		// to do so. So we keep the `LastAsyncOperation` condition.
   127  		// TODO: move this to the `Synced` condition.
   128  		tr.SetConditions(resource.LastAsyncOperationCondition(err))
   129  		if err != nil {
   130  			wrapMsg := ""
   131  			switch op {
   132  			case "create":
   133  				wrapMsg = errXPReconcileCreate
   134  			case "update":
   135  				wrapMsg = errXPReconcileUpdate
   136  			case "destroy":
   137  				wrapMsg = errXPReconcileDelete
   138  			}
   139  			tr.SetConditions(xpv1.ReconcileError(errors.Wrap(err, wrapMsg)))
   140  		} else {
   141  			tr.SetConditions(xpv1.ReconcileSuccess())
   142  		}
   143  		if ac.enableStatusUpdates {
   144  			tr.SetConditions(resource.AsyncOperationFinishedCondition())
   145  		}
   146  		uErr := errors.Wrapf(ac.kube.Status().Update(ctx, tr), errUpdateStatusFmt, tr.GetObjectKind().GroupVersionKind().String(), name, op)
   147  		if ac.eventHandler != nil {
   148  			rateLimiter := handler.NoRateLimiter
   149  			switch {
   150  			case err != nil:
   151  				rateLimiter = rateLimiterCallback
   152  			default:
   153  				ac.eventHandler.Forget(rateLimiterCallback, name)
   154  			}
   155  			// TODO: use the errors.Join from
   156  			// github.com/crossplane/crossplane-runtime.
   157  			if ok := ac.eventHandler.RequestReconcile(rateLimiter, name, nil); !ok {
   158  				return errors.Errorf(errReconcileRequestFmt, tr.GetObjectKind().GroupVersionKind().String(), name, op)
   159  			}
   160  		}
   161  		return uErr
   162  	}
   163  }
   164  
   165  // Create makes sure the error is saved in async operation condition.
   166  func (ac *APICallbacks) Create(name string) terraform.CallbackFn {
   167  	// request will be requeued although the managed reconciler already
   168  	// requeues with exponential back-off during the creation phase
   169  	// because the upjet external client returns ResourceExists &
   170  	// ResourceUpToDate both set to true, if an async operation is
   171  	// in-progress immediately following a Create call. This will
   172  	// delay a reobservation of the resource (while being created)
   173  	// for the poll period.
   174  	return ac.callbackFn(name, "create")
   175  }
   176  
   177  // Update makes sure the error is saved in async operation condition.
   178  func (ac *APICallbacks) Update(name string) terraform.CallbackFn {
   179  	return ac.callbackFn(name, "update")
   180  }
   181  
   182  // Destroy makes sure the error is saved in async operation condition.
   183  func (ac *APICallbacks) Destroy(name string) terraform.CallbackFn {
   184  	// request will be requeued although the managed reconciler requeues
   185  	// with exponential back-off during the deletion phase because
   186  	// during the async deletion operation, external client's
   187  	// observe just returns success to the managed reconciler.
   188  	return ac.callbackFn(name, "destroy")
   189  }