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 }