-
Notifications
You must be signed in to change notification settings - Fork 0
/
claim_reconciler.go
407 lines (340 loc) · 16.4 KB
/
claim_reconciler.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
/*
Copyright 2019 The Crossplane Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resource
import (
"context"
"time"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/crossplaneio/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplaneio/crossplane-runtime/pkg/logging"
"github.com/crossplaneio/crossplane-runtime/pkg/meta"
)
const (
claimControllerName = "resourceclaim.crossplane.io"
claimReconcileTimeout = 1 * time.Minute
aShortWait = 30 * time.Second
)
// Reasons a resource claim is or is not ready.
const (
ReasonBinding = "Managed claim is waiting for managed resource to become bindable"
)
// Error strings.
const (
errGetClaim = "cannot get resource claim"
errUpdateClaimStatus = "cannot update resource claim status"
)
var log = logging.Logger.WithName("controller")
// A ClaimKind contains the type metadata for a kind of resource claim.
type ClaimKind schema.GroupVersionKind
// A ClassKind contains the type metadata for a kind of resource class.
type ClassKind schema.GroupVersionKind
// A ManagedKind contains the type metadata for a kind of managed resource.
type ManagedKind schema.GroupVersionKind
// A ManagedConfigurator configures a resource, typically by converting it to
// a known type and populating its spec.
type ManagedConfigurator interface {
Configure(ctx context.Context, cm Claim, cs Class, mg Managed) error
}
// A ManagedConfiguratorFn is a function that sastisfies the
// ManagedConfigurator interface.
type ManagedConfiguratorFn func(ctx context.Context, cm Claim, cs Class, mg Managed) error
// Configure the supplied resource using the supplied claim and class.
func (fn ManagedConfiguratorFn) Configure(ctx context.Context, cm Claim, cs Class, mg Managed) error {
return fn(ctx, cm, cs, mg)
}
// A ManagedCreator creates a resource, typically by submitting it to an API
// server. ManagedCreators must not modify the supplied resource class, but are
// responsible for final modifications to the claim and resource, for example
// ensuring resource, class, claim, and owner references are set.
type ManagedCreator interface {
Create(ctx context.Context, cm Claim, cs Class, mg Managed) error
}
// A ManagedCreatorFn is a function that sastisfies the ManagedCreator interface.
type ManagedCreatorFn func(ctx context.Context, cm Claim, cs Class, mg Managed) error
// Create the supplied resource.
func (fn ManagedCreatorFn) Create(ctx context.Context, cm Claim, cs Class, mg Managed) error {
return fn(ctx, cm, cs, mg)
}
// A ManagedConnectionPropagator is responsible for propagating information
// required to connect to a managed resource (for example the connection secret)
// from the managed resource to its resource claim.
type ManagedConnectionPropagator interface {
PropagateConnection(ctx context.Context, cm Claim, mg Managed) error
}
// A ManagedConnectionPropagatorFn is a function that sastisfies the
// ManagedConnectionPropagator interface.
type ManagedConnectionPropagatorFn func(ctx context.Context, cm Claim, mg Managed) error
// PropagateConnection information from the supplied managed resource to the
// supplied resource claim.
func (fn ManagedConnectionPropagatorFn) PropagateConnection(ctx context.Context, cm Claim, mg Managed) error {
return fn(ctx, cm, mg)
}
// A ManagedBinder binds a resource claim to a managed resource.
type ManagedBinder interface {
Bind(ctx context.Context, cm Claim, mg Managed) error
}
// A ManagedBinderFn is a function that sastisfies the ManagedBinder interface.
type ManagedBinderFn func(ctx context.Context, cm Claim, mg Managed) error
// Bind the supplied resource claim to the supplied managed resource.
func (fn ManagedBinderFn) Bind(ctx context.Context, cm Claim, mg Managed) error {
return fn(ctx, cm, mg)
}
// A ManagedFinalizer finalizes the deletion of a resource claim.
type ManagedFinalizer interface {
Finalize(ctx context.Context, cm Managed) error
}
// A ManagedFinalizerFn is a function that sastisfies the ManagedFinalizer interface.
type ManagedFinalizerFn func(ctx context.Context, cm Managed) error
// Finalize the supplied managed resource.
func (fn ManagedFinalizerFn) Finalize(ctx context.Context, cm Managed) error {
return fn(ctx, cm)
}
// A ClaimFinalizer finalizes the deletion of a resource claim.
type ClaimFinalizer interface {
Finalize(ctx context.Context, cm Claim) error
}
// A ClaimFinalizerFn is a function that sastisfies the ClaimFinalizer interface.
type ClaimFinalizerFn func(ctx context.Context, cm Claim) error
// Finalize the supplied managed resource.
func (fn ClaimFinalizerFn) Finalize(ctx context.Context, cm Claim) error {
return fn(ctx, cm)
}
// A ClaimReconciler reconciles resource claims by creating exactly one kind of
// concrete managed resource. Each resource claim kind should create an instance
// of this controller for each managed resource kind they can bind to, using
// watch predicates to ensure each controller is responsible for exactly one
// type of resource class provisioner. Each controller must watch its subset of
// resource claims and any managed resources they control.
type ClaimReconciler struct {
client client.Client
newClaim func() Claim
newClass func() Class
newManaged func() Managed
// The below structs embed the set of interfaces used to implement the
// resource claim reconciler. We do this primarily for readability, so that
// the reconciler logic reads r.managed.Create(), r.claim.Finalize(), etc.
managed crManaged
claim crClaim
}
type crManaged struct {
ManagedConfigurator
ManagedCreator
ManagedConnectionPropagator
ManagedBinder
ManagedFinalizer
}
func defaultCRManaged(m manager.Manager) crManaged {
return crManaged{
ManagedConfigurator: NewObjectMetaConfigurator(m.GetScheme()),
ManagedCreator: NewAPIManagedCreator(m.GetClient(), m.GetScheme()),
ManagedConnectionPropagator: NewAPIManagedConnectionPropagator(m.GetClient(), m.GetScheme()),
ManagedBinder: NewAPIManagedBinder(m.GetClient()),
ManagedFinalizer: NewAPIManagedUnbinder(m.GetClient()),
}
}
type crClaim struct {
ClaimFinalizer
}
func defaultCRClaim(m manager.Manager) crClaim {
return crClaim{ClaimFinalizer: NewAPIClaimFinalizerRemover(m.GetClient())}
}
// A ClaimReconcilerOption configures a Reconciler.
type ClaimReconcilerOption func(*ClaimReconciler)
// WithManagedConfigurators specifies which configurators should be used to
// configure each managed resource. Configurators will be applied in the order
// they are specified.
func WithManagedConfigurators(c ...ManagedConfigurator) ClaimReconcilerOption {
return func(r *ClaimReconciler) {
r.managed.ManagedConfigurator = ConfiguratorChain(c)
}
}
// WithManagedCreator specifies which ManagedCreator should be used to create
// managed resources.
func WithManagedCreator(c ManagedCreator) ClaimReconcilerOption {
return func(r *ClaimReconciler) {
r.managed.ManagedCreator = c
}
}
// WithManagedConnectionPropagator specifies which ManagedConnectionPropagator
// should be used to propagate resource connection details to their claim.
func WithManagedConnectionPropagator(p ManagedConnectionPropagator) ClaimReconcilerOption {
return func(r *ClaimReconciler) {
r.managed.ManagedConnectionPropagator = p
}
}
// WithManagedBinder specifies which ManagedBinder should be used to bind
// resources to their claim.
func WithManagedBinder(b ManagedBinder) ClaimReconcilerOption {
return func(r *ClaimReconciler) {
r.managed.ManagedBinder = b
}
}
// WithManagedFinalizer specifies which ManagedFinalizer should be used to
// finalize managed resources when their claims are deleted.
func WithManagedFinalizer(f ManagedFinalizer) ClaimReconcilerOption {
return func(r *ClaimReconciler) {
r.managed.ManagedFinalizer = f
}
}
// WithClaimFinalizer specifies which ClaimFinalizer should be used to finalize
// claims when they are deleted.
func WithClaimFinalizer(f ClaimFinalizer) ClaimReconcilerOption {
return func(r *ClaimReconciler) {
r.claim.ClaimFinalizer = f
}
}
// NewClaimReconciler returns a ClaimReconciler that reconciles resource claims
// of the supplied ClaimKind with resources of the supplied ManagedKind. It
// panics if asked to reconcile a claim or resource kind that is not registered
// with the supplied manager's runtime.Scheme. The returned ClaimReconciler will
// apply only the ObjectMetaConfigurator by default; most callers should supply
// one or more ManagedConfigurators to configure their managed resources.
func NewClaimReconciler(m manager.Manager, of ClaimKind, using ClassKind, with ManagedKind, o ...ClaimReconcilerOption) *ClaimReconciler {
nc := func() Claim { return MustCreateObject(schema.GroupVersionKind(of), m.GetScheme()).(Claim) }
ns := func() Class { return MustCreateObject(schema.GroupVersionKind(using), m.GetScheme()).(Class) }
nr := func() Managed { return MustCreateObject(schema.GroupVersionKind(with), m.GetScheme()).(Managed) }
// Panic early if we've been asked to reconcile a claim or resource kind
// that has not been registered with our controller manager's scheme.
_, _, _ = nc(), ns(), nr()
r := &ClaimReconciler{
client: m.GetClient(),
newClaim: nc,
newClass: ns,
newManaged: nr,
managed: defaultCRManaged(m),
claim: defaultCRClaim(m),
}
for _, ro := range o {
ro(r)
}
return r
}
// Reconcile a resource claim with a concrete managed resource.
func (r *ClaimReconciler) Reconcile(req reconcile.Request) (reconcile.Result, error) { // nolint:gocyclo
// NOTE(negz): This method is a little over our cyclomatic complexity goal.
// Be wary of adding additional complexity.
log.V(logging.Debug).Info("Reconciling", "controller", claimControllerName, "request", req)
ctx, cancel := context.WithTimeout(context.Background(), claimReconcileTimeout)
defer cancel()
claim := r.newClaim()
if err := r.client.Get(ctx, req.NamespacedName, claim); err != nil {
// There's no need to requeue if we no longer exist. Otherwise we'll be
// requeued implicitly because we return an error.
return reconcile.Result{}, errors.Wrap(IgnoreNotFound(err), errGetClaim)
}
managed := r.newManaged()
if ref := claim.GetResourceReference(); ref != nil {
if err := IgnoreNotFound(r.client.Get(ctx, meta.NamespacedNameOf(ref), managed)); err != nil {
// If we didn't hit this error last time we'll be requeued
// implicitly due to the status update. Otherwise we want to retry
// after a brief wait, in case this was a transient error.
claim.SetConditions(v1alpha1.ReconcileError(err))
return reconcile.Result{RequeueAfter: aShortWait}, errors.Wrap(r.client.Status().Update(ctx, claim), errUpdateClaimStatus)
}
}
if meta.WasDeleted(claim) {
if err := r.managed.Finalize(ctx, managed); err != nil {
// If we didn't hit this error last time we'll be requeued
// implicitly due to the status update. Otherwise we want to retry
// after a brief wait, in case this was a transient error.
claim.SetConditions(v1alpha1.Deleting(), v1alpha1.ReconcileError(err))
return reconcile.Result{RequeueAfter: aShortWait}, errors.Wrap(r.client.Status().Update(ctx, claim), errUpdateClaimStatus)
}
if err := r.claim.Finalize(ctx, claim); err != nil {
// If we didn't hit this error last time we'll be requeued
// implicitly due to the status update. Otherwise we want to retry
// after a brief wait, in case this was a transient error.
claim.SetConditions(v1alpha1.Deleting(), v1alpha1.ReconcileError(err))
return reconcile.Result{RequeueAfter: aShortWait}, errors.Wrap(r.client.Status().Update(ctx, claim), errUpdateClaimStatus)
}
// We've successfully processed the delete, so there's no further
// reconciliation to do. There's a good chance our claim no longer
// exists, but we try update its status just in case it sticks around,
// for example due to additional finalizers.
claim.SetConditions(v1alpha1.Deleting(), v1alpha1.ReconcileSuccess())
return reconcile.Result{Requeue: false}, errors.Wrap(IgnoreNotFound(r.client.Status().Update(ctx, claim)), errUpdateClaimStatus)
}
if !meta.WasCreated(managed) {
class := r.newClass()
// Class reference should always be set by the time we get this far; our
// watch predicates require it.
if err := r.client.Get(ctx, meta.NamespacedNameOf(claim.GetClassReference()), class); err != nil {
// If we didn't hit this error last time we'll be requeued
// implicitly due to the status update. Otherwise we want to retry
// after a brief wait, in case this was a transient error or the
// class is (re)created.
claim.SetConditions(v1alpha1.Creating(), v1alpha1.ReconcileError(err))
return reconcile.Result{RequeueAfter: aShortWait}, errors.Wrap(r.client.Status().Update(ctx, claim), errUpdateClaimStatus)
}
if err := r.managed.Configure(ctx, claim, class, managed); err != nil {
// If we didn't hit this error last time we'll be requeued
// implicitly due to the status update. Otherwise we want to retry
// after a brief wait, in case this was a transient error or some
// issue with the resource class was resolved.
claim.SetConditions(v1alpha1.Creating(), v1alpha1.ReconcileError(err))
return reconcile.Result{RequeueAfter: aShortWait}, errors.Wrap(r.client.Status().Update(ctx, claim), errUpdateClaimStatus)
}
if err := r.managed.Create(ctx, claim, class, managed); err != nil {
// If we didn't hit this error last time we'll be requeued
// implicitly due to the status update. Otherwise we want to retry
// after a brief wait, in case this was a transient error.
claim.SetConditions(v1alpha1.Creating(), v1alpha1.ReconcileError(err))
return reconcile.Result{RequeueAfter: aShortWait}, errors.Wrap(r.client.Status().Update(ctx, claim), errUpdateClaimStatus)
}
}
if !IsBindable(managed) && !IsBound(managed) {
// If this claim was not already binding we'll be requeued due to the
// status update. Otherwise there's no need to requeue. We should be
// watching both the resource claims and the resources we own, so we'll
// be queued if anything changes.
claim.SetConditions(Binding(), v1alpha1.ReconcileSuccess())
return reconcile.Result{Requeue: false}, errors.Wrap(r.client.Status().Update(ctx, claim), errUpdateClaimStatus)
}
if IsBindable(managed) {
if err := r.managed.PropagateConnection(ctx, claim, managed); err != nil {
// If we didn't hit this error last time we'll be requeued implicitly
// due to the status update. Otherwise we want to retry after a brief
// wait in case this was a transient error, or the resource connection
// secret is created.
claim.SetConditions(Binding(), v1alpha1.ReconcileError(err))
return reconcile.Result{RequeueAfter: aShortWait}, errors.Wrap(r.client.Status().Update(ctx, claim), errUpdateClaimStatus)
}
if err := r.managed.Bind(ctx, claim, managed); err != nil {
// If we didn't hit this error last time we'll be requeued implicitly
// due to the status update. Otherwise we want to retry after a brief
// wait, in case this was a transient error.
claim.SetConditions(Binding(), v1alpha1.ReconcileError(err))
return reconcile.Result{RequeueAfter: aShortWait}, errors.Wrap(r.client.Status().Update(ctx, claim), errUpdateClaimStatus)
}
}
// No need to requeue. We should be watching both the resource claims and
// the resources we own, so we'll be queued if anything changes.
claim.SetConditions(v1alpha1.Available(), v1alpha1.ReconcileSuccess())
return reconcile.Result{Requeue: false}, errors.Wrap(r.client.Status().Update(ctx, claim), errUpdateClaimStatus)
}
// Binding returns a condition that indicates the resource claim is currently
// waiting for its managed resource to become bindable.
func Binding() v1alpha1.Condition {
return v1alpha1.Condition{
Type: v1alpha1.TypeReady,
Status: corev1.ConditionFalse,
LastTransitionTime: metav1.Now(),
Reason: ReasonBinding,
}
}