-
Notifications
You must be signed in to change notification settings - Fork 103
/
reconciler.go
1367 lines (1189 loc) · 62.2 KB
/
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
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
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 managed
import (
"context"
"math/rand"
"strings"
"time"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/crossplane/crossplane-runtime/apis/changelogs/proto/v1alpha1"
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/event"
"github.com/crossplane/crossplane-runtime/pkg/feature"
"github.com/crossplane/crossplane-runtime/pkg/logging"
"github.com/crossplane/crossplane-runtime/pkg/meta"
"github.com/crossplane/crossplane-runtime/pkg/resource"
)
const (
// FinalizerName is the string that is used as finalizer on managed resource
// objects.
FinalizerName = "finalizer.managedresource.crossplane.io"
reconcileGracePeriod = 30 * time.Second
reconcileTimeout = 1 * time.Minute
defaultPollInterval = 1 * time.Minute
defaultGracePeriod = 30 * time.Second
)
// Error strings.
const (
errFmtManagementPolicyNonDefault = "`spec.managementPolicies` is set to a non-default value but the feature is not enabled: %s"
errFmtManagementPolicyNotSupported = "`spec.managementPolicies` is set to a value(%s) which is not supported. Check docs for supported policies"
errGetManaged = "cannot get managed resource"
errUpdateManagedAnnotations = "cannot update managed resource annotations"
errCreateIncomplete = "cannot determine creation result - remove the " + meta.AnnotationKeyExternalCreatePending + " annotation if it is safe to proceed"
errReconcileConnect = "connect failed"
errReconcileObserve = "observe failed"
errReconcileCreate = "create failed"
errReconcileUpdate = "update failed"
errReconcileDelete = "delete failed"
errRecordChangeLog = "cannot record change log entry"
errExternalResourceNotExist = "external resource does not exist"
)
// Event reasons.
const (
reasonCannotConnect event.Reason = "CannotConnectToProvider"
reasonCannotDisconnect event.Reason = "CannotDisconnectFromProvider"
reasonCannotInitialize event.Reason = "CannotInitializeManagedResource"
reasonCannotResolveRefs event.Reason = "CannotResolveResourceReferences"
reasonCannotObserve event.Reason = "CannotObserveExternalResource"
reasonCannotCreate event.Reason = "CannotCreateExternalResource"
reasonCannotDelete event.Reason = "CannotDeleteExternalResource"
reasonCannotPublish event.Reason = "CannotPublishConnectionDetails"
reasonCannotUnpublish event.Reason = "CannotUnpublishConnectionDetails"
reasonCannotUpdate event.Reason = "CannotUpdateExternalResource"
reasonCannotUpdateManaged event.Reason = "CannotUpdateManagedResource"
reasonManagementPolicyInvalid event.Reason = "CannotUseInvalidManagementPolicy"
reasonDeleted event.Reason = "DeletedExternalResource"
reasonCreated event.Reason = "CreatedExternalResource"
reasonUpdated event.Reason = "UpdatedExternalResource"
reasonPending event.Reason = "PendingExternalResource"
reasonReconciliationPaused event.Reason = "ReconciliationPaused"
)
// ControllerName returns the recommended name for controllers that use this
// package to reconcile a particular kind of managed resource.
func ControllerName(kind string) string {
return "managed/" + strings.ToLower(kind)
}
// ManagementPoliciesChecker is used to perform checks on management policies
// to determine specific actions are allowed, or if they are the only allowed
// action.
type ManagementPoliciesChecker interface { //nolint:interfacebloat // This has to be big.
// Validate validates the management policies.
Validate() error
// IsPaused returns true if the resource is paused based
// on the management policy.
IsPaused() bool
// ShouldOnlyObserve returns true if only the Observe action is allowed.
ShouldOnlyObserve() bool
// ShouldCreate returns true if the Create action is allowed.
ShouldCreate() bool
// ShouldLateInitialize returns true if the LateInitialize action is
// allowed.
ShouldLateInitialize() bool
// ShouldUpdate returns true if the Update action is allowed.
ShouldUpdate() bool
// ShouldDelete returns true if the Delete action is allowed.
ShouldDelete() bool
}
// A CriticalAnnotationUpdater is used when it is critical that annotations must
// be updated before returning from the Reconcile loop.
type CriticalAnnotationUpdater interface {
UpdateCriticalAnnotations(ctx context.Context, o client.Object) error
}
// A CriticalAnnotationUpdateFn may be used when it is critical that annotations
// must be updated before returning from the Reconcile loop.
type CriticalAnnotationUpdateFn func(ctx context.Context, o client.Object) error
// UpdateCriticalAnnotations of the supplied object.
func (fn CriticalAnnotationUpdateFn) UpdateCriticalAnnotations(ctx context.Context, o client.Object) error {
return fn(ctx, o)
}
// ConnectionDetails created or updated during an operation on an external
// resource, for example usernames, passwords, endpoints, ports, etc.
type ConnectionDetails map[string][]byte
// AdditionalDetails represent any additional details the external client wants
// to return about an operation that has been performed. These details will be
// included in the change logs.
type AdditionalDetails map[string]string
// A ConnectionPublisher manages the supplied ConnectionDetails for the
// supplied Managed resource. ManagedPublishers must handle the case in which
// the supplied ConnectionDetails are empty.
type ConnectionPublisher interface {
// PublishConnection details for the supplied Managed resource. Publishing
// must be additive; i.e. if details (a, b, c) are published, subsequently
// publicing details (b, c, d) should update (b, c) but not remove a.
PublishConnection(ctx context.Context, so resource.ConnectionSecretOwner, c ConnectionDetails) (published bool, err error)
// UnpublishConnection details for the supplied Managed resource.
UnpublishConnection(ctx context.Context, so resource.ConnectionSecretOwner, c ConnectionDetails) error
}
// ConnectionPublisherFns is the pluggable struct to produce objects with ConnectionPublisher interface.
type ConnectionPublisherFns struct {
PublishConnectionFn func(ctx context.Context, o resource.ConnectionSecretOwner, c ConnectionDetails) (bool, error)
UnpublishConnectionFn func(ctx context.Context, o resource.ConnectionSecretOwner, c ConnectionDetails) error
}
// PublishConnection details for the supplied Managed resource.
func (fn ConnectionPublisherFns) PublishConnection(ctx context.Context, o resource.ConnectionSecretOwner, c ConnectionDetails) (bool, error) {
return fn.PublishConnectionFn(ctx, o, c)
}
// UnpublishConnection details for the supplied Managed resource.
func (fn ConnectionPublisherFns) UnpublishConnection(ctx context.Context, o resource.ConnectionSecretOwner, c ConnectionDetails) error {
return fn.UnpublishConnectionFn(ctx, o, c)
}
// A ConnectionDetailsFetcher fetches connection details for the supplied
// Connection Secret owner.
type ConnectionDetailsFetcher interface {
FetchConnection(ctx context.Context, so resource.ConnectionSecretOwner) (ConnectionDetails, error)
}
// A Initializer establishes ownership of the supplied Managed resource.
// This typically involves the operations that are run before calling any
// ExternalClient methods.
type Initializer interface {
Initialize(ctx context.Context, mg resource.Managed) error
}
// A InitializerChain chains multiple managed initializers.
type InitializerChain []Initializer
// Initialize calls each Initializer serially. It returns the first
// error it encounters, if any.
func (cc InitializerChain) Initialize(ctx context.Context, mg resource.Managed) error {
for _, c := range cc {
if err := c.Initialize(ctx, mg); err != nil {
return err
}
}
return nil
}
// A InitializerFn is a function that satisfies the Initializer
// interface.
type InitializerFn func(ctx context.Context, mg resource.Managed) error
// Initialize calls InitializerFn function.
func (m InitializerFn) Initialize(ctx context.Context, mg resource.Managed) error {
return m(ctx, mg)
}
// A ReferenceResolver resolves references to other managed resources.
type ReferenceResolver interface {
// ResolveReferences resolves all fields in the supplied managed resource
// that are references to other managed resources by updating corresponding
// fields, for example setting spec.network to the Network resource
// specified by spec.networkRef.name.
ResolveReferences(ctx context.Context, mg resource.Managed) error
}
// A ReferenceResolverFn is a function that satisfies the
// ReferenceResolver interface.
type ReferenceResolverFn func(context.Context, resource.Managed) error
// ResolveReferences calls ReferenceResolverFn function.
func (m ReferenceResolverFn) ResolveReferences(ctx context.Context, mg resource.Managed) error {
return m(ctx, mg)
}
// An ExternalConnecter produces a new ExternalClient given the supplied
// Managed resource.
type ExternalConnecter = TypedExternalConnecter[resource.Managed]
// A TypedExternalConnecter produces a new ExternalClient given the supplied
// Managed resource.
type TypedExternalConnecter[managed resource.Managed] interface {
// Connect to the provider specified by the supplied managed resource and
// produce an ExternalClient.
Connect(ctx context.Context, mg managed) (TypedExternalClient[managed], error)
}
// An ExternalDisconnecter disconnects from a provider.
//
// Deprecated: Please use Disconnect() on the ExternalClient for disconnecting
// from the provider.
type ExternalDisconnecter interface {
// Disconnect from the provider and close the ExternalClient.
Disconnect(ctx context.Context) error
}
// A NopDisconnecter converts an ExternalConnecter into an
// ExternalConnectDisconnecter with a no-op Disconnect method.
type NopDisconnecter = TypedNopDisconnecter[resource.Managed]
// A TypedNopDisconnecter converts an ExternalConnecter into an
// ExternalConnectDisconnecter with a no-op Disconnect method.
type TypedNopDisconnecter[managed resource.Managed] struct {
c TypedExternalConnecter[managed]
}
// Connect calls the underlying ExternalConnecter's Connect method.
func (c *TypedNopDisconnecter[managed]) Connect(ctx context.Context, mg managed) (TypedExternalClient[managed], error) {
return c.c.Connect(ctx, mg)
}
// Disconnect does nothing. It never returns an error.
func (c *TypedNopDisconnecter[managed]) Disconnect(_ context.Context) error {
return nil
}
// NewNopDisconnecter converts an ExternalConnecter into an
// ExternalConnectDisconnecter with a no-op Disconnect method.
func NewNopDisconnecter(c ExternalConnecter) ExternalConnectDisconnecter {
return NewTypedNopDisconnecter(c)
}
// NewTypedNopDisconnecter converts an TypedExternalConnecter into an
// ExternalConnectDisconnecter with a no-op Disconnect method.
func NewTypedNopDisconnecter[managed resource.Managed](c TypedExternalConnecter[managed]) TypedExternalConnectDisconnecter[managed] {
return &TypedNopDisconnecter[managed]{c}
}
// An ExternalConnectDisconnecter produces a new ExternalClient given the supplied
// Managed resource.
type ExternalConnectDisconnecter = TypedExternalConnectDisconnecter[resource.Managed]
// A TypedExternalConnectDisconnecter produces a new ExternalClient given the supplied
// Managed resource.
type TypedExternalConnectDisconnecter[managed resource.Managed] interface {
TypedExternalConnecter[managed]
ExternalDisconnecter
}
// An ExternalConnectorFn is a function that satisfies the ExternalConnecter
// interface.
type ExternalConnectorFn = TypedExternalConnectorFn[resource.Managed]
// An TypedExternalConnectorFn is a function that satisfies the
// TypedExternalConnecter interface.
type TypedExternalConnectorFn[managed resource.Managed] func(ctx context.Context, mg managed) (TypedExternalClient[managed], error)
// Connect to the provider specified by the supplied managed resource and
// produce an ExternalClient.
func (ec TypedExternalConnectorFn[managed]) Connect(ctx context.Context, mg managed) (TypedExternalClient[managed], error) {
return ec(ctx, mg)
}
// An ExternalDisconnectorFn is a function that satisfies the ExternalConnecter
// interface.
type ExternalDisconnectorFn func(ctx context.Context) error
// Disconnect from provider and close the ExternalClient.
func (ed ExternalDisconnectorFn) Disconnect(ctx context.Context) error {
return ed(ctx)
}
// ExternalConnectDisconnecterFns are functions that satisfy the
// ExternalConnectDisconnecter interface.
type ExternalConnectDisconnecterFns = TypedExternalConnectDisconnecterFns[resource.Managed]
// TypedExternalConnectDisconnecterFns are functions that satisfy the
// TypedExternalConnectDisconnecter interface.
type TypedExternalConnectDisconnecterFns[managed resource.Managed] struct {
ConnectFn func(ctx context.Context, mg managed) (TypedExternalClient[managed], error)
DisconnectFn func(ctx context.Context) error
}
// Connect to the provider specified by the supplied managed resource and
// produce an ExternalClient.
func (fns TypedExternalConnectDisconnecterFns[managed]) Connect(ctx context.Context, mg managed) (TypedExternalClient[managed], error) {
return fns.ConnectFn(ctx, mg)
}
// Disconnect from the provider and close the ExternalClient.
func (fns TypedExternalConnectDisconnecterFns[managed]) Disconnect(ctx context.Context) error {
return fns.DisconnectFn(ctx)
}
// An ExternalClient manages the lifecycle of an external resource.
// None of the calls here should be blocking. All of the calls should be
// idempotent. For example, Create call should not return AlreadyExists error
// if it's called again with the same parameters or Delete call should not
// return error if there is an ongoing deletion or resource does not exist.
type ExternalClient = TypedExternalClient[resource.Managed]
// A TypedExternalClient manages the lifecycle of an external resource.
// None of the calls here should be blocking. All of the calls should be
// idempotent. For example, Create call should not return AlreadyExists error
// if it's called again with the same parameters or Delete call should not
// return error if there is an ongoing deletion or resource does not exist.
type TypedExternalClient[managedType resource.Managed] interface {
// Observe the external resource the supplied Managed resource
// represents, if any. Observe implementations must not modify the
// external resource, but may update the supplied Managed resource to
// reflect the state of the external resource. Status modifications are
// automatically persisted unless ResourceLateInitialized is true - see
// ResourceLateInitialized for more detail.
Observe(ctx context.Context, mg managedType) (ExternalObservation, error)
// Create an external resource per the specifications of the supplied
// Managed resource. Called when Observe reports that the associated
// external resource does not exist. Create implementations may update
// managed resource annotations, and those updates will be persisted.
// All other updates will be discarded.
Create(ctx context.Context, mg managedType) (ExternalCreation, error)
// Update the external resource represented by the supplied Managed
// resource, if necessary. Called unless Observe reports that the
// associated external resource is up to date.
Update(ctx context.Context, mg managedType) (ExternalUpdate, error)
// Delete the external resource upon deletion of its associated Managed
// resource. Called when the managed resource has been deleted.
Delete(ctx context.Context, mg managedType) (ExternalDelete, error)
// Disconnect from the provider and close the ExternalClient.
// Called at the end of reconcile loop. An ExternalClient not requiring
// to explicitly disconnect to cleanup it resources, can provide a no-op
// implementation which just return nil.
Disconnect(ctx context.Context) error
}
// ExternalClientFns are a series of functions that satisfy the ExternalClient
// interface.
type ExternalClientFns = TypedExternalClientFns[resource.Managed]
// TypedExternalClientFns are a series of functions that satisfy the
// ExternalClient interface.
type TypedExternalClientFns[managed resource.Managed] struct {
ObserveFn func(ctx context.Context, mg managed) (ExternalObservation, error)
CreateFn func(ctx context.Context, mg managed) (ExternalCreation, error)
UpdateFn func(ctx context.Context, mg managed) (ExternalUpdate, error)
DeleteFn func(ctx context.Context, mg managed) (ExternalDelete, error)
DisconnectFn func(ctx context.Context) error
}
// Observe the external resource the supplied Managed resource represents, if
// any.
func (e TypedExternalClientFns[managed]) Observe(ctx context.Context, mg managed) (ExternalObservation, error) {
return e.ObserveFn(ctx, mg)
}
// Create an external resource per the specifications of the supplied Managed
// resource.
func (e TypedExternalClientFns[managed]) Create(ctx context.Context, mg managed) (ExternalCreation, error) {
return e.CreateFn(ctx, mg)
}
// Update the external resource represented by the supplied Managed resource, if
// necessary.
func (e TypedExternalClientFns[managed]) Update(ctx context.Context, mg managed) (ExternalUpdate, error) {
return e.UpdateFn(ctx, mg)
}
// Delete the external resource upon deletion of its associated Managed
// resource.
func (e TypedExternalClientFns[managed]) Delete(ctx context.Context, mg managed) (ExternalDelete, error) {
return e.DeleteFn(ctx, mg)
}
// Disconnect the external client.
func (e TypedExternalClientFns[managed]) Disconnect(ctx context.Context) error {
return e.DisconnectFn(ctx)
}
// A NopConnecter does nothing.
type NopConnecter struct{}
// Connect returns a NopClient. It never returns an error.
func (c *NopConnecter) Connect(_ context.Context, _ resource.Managed) (ExternalClient, error) {
return &NopClient{}, nil
}
// A NopClient does nothing.
type NopClient struct{}
// Observe does nothing. It returns an empty ExternalObservation and no error.
func (c *NopClient) Observe(_ context.Context, _ resource.Managed) (ExternalObservation, error) {
return ExternalObservation{}, nil
}
// Create does nothing. It returns an empty ExternalCreation and no error.
func (c *NopClient) Create(_ context.Context, _ resource.Managed) (ExternalCreation, error) {
return ExternalCreation{}, nil
}
// Update does nothing. It returns an empty ExternalUpdate and no error.
func (c *NopClient) Update(_ context.Context, _ resource.Managed) (ExternalUpdate, error) {
return ExternalUpdate{}, nil
}
// Delete does nothing. It never returns an error.
func (c *NopClient) Delete(_ context.Context, _ resource.Managed) (ExternalDelete, error) {
return ExternalDelete{}, nil
}
// Disconnect does nothing. It never returns an error.
func (c *NopClient) Disconnect(_ context.Context) error { return nil }
// An ExternalObservation is the result of an observation of an external
// resource.
type ExternalObservation struct {
// ResourceExists must be true if a corresponding external resource exists
// for the managed resource. Typically this is proven by the presence of an
// external resource of the expected kind whose unique identifier matches
// the managed resource's external name. Crossplane uses this information to
// determine whether it needs to create or delete the external resource.
ResourceExists bool
// ResourceUpToDate should be true if the corresponding external resource
// appears to be up-to-date - i.e. updating the external resource to match
// the desired state of the managed resource would be a no-op. Keep in mind
// that often only a subset of external resource fields can be updated.
// Crossplane uses this information to determine whether it needs to update
// the external resource.
ResourceUpToDate bool
// ResourceLateInitialized should be true if the managed resource's spec was
// updated during its observation. A Crossplane provider may update a
// managed resource's spec fields after it is created or updated, as long as
// the updates are limited to setting previously unset fields, and adding
// keys to maps. Crossplane uses this information to determine whether
// changes to the spec were made during observation that must be persisted.
// Note that changes to the spec will be persisted before changes to the
// status, and that pending changes to the status may be lost when the spec
// is persisted. Status changes will be persisted by the first subsequent
// observation that _does not_ late initialize the managed resource, so it
// is important that Observe implementations do not late initialize the
// resource every time they are called.
ResourceLateInitialized bool
// ConnectionDetails required to connect to this resource. These details
// are a set that is collated throughout the managed resource's lifecycle -
// i.e. returning new connection details will have no affect on old details
// unless an existing key is overwritten. Crossplane may publish these
// credentials to a store (e.g. a Secret).
ConnectionDetails ConnectionDetails
// Diff is a Debug level message that is sent to the reconciler when
// there is a change in the observed Managed Resource. It is useful for
// finding where the observed diverges from the desired state.
// The string should be a cmp.Diff that details the difference.
Diff string
}
// An ExternalCreation is the result of the creation of an external resource.
type ExternalCreation struct {
// ConnectionDetails required to connect to this resource. These details
// are a set that is collated throughout the managed resource's lifecycle -
// i.e. returning new connection details will have no affect on old details
// unless an existing key is overwritten. Crossplane may publish these
// credentials to a store (e.g. a Secret).
ConnectionDetails ConnectionDetails
// AdditionalDetails represent any additional details the external client
// wants to return about the creation operation that was performed.
AdditionalDetails AdditionalDetails
}
// An ExternalUpdate is the result of an update to an external resource.
type ExternalUpdate struct {
// ConnectionDetails required to connect to this resource. These details
// are a set that is collated throughout the managed resource's lifecycle -
// i.e. returning new connection details will have no affect on old details
// unless an existing key is overwritten. Crossplane may publish these
// credentials to a store (e.g. a Secret).
ConnectionDetails ConnectionDetails
// AdditionalDetails represent any additional details the external client
// wants to return about the update operation that was performed.
AdditionalDetails AdditionalDetails
}
// An ExternalDelete is the result of a deletion of an external resource.
type ExternalDelete struct {
// AdditionalDetails represent any additional details the external client
// wants to return about the delete operation that was performed.
AdditionalDetails AdditionalDetails
}
// A Reconciler reconciles managed resources by creating and managing the
// lifecycle of an external resource, i.e. a resource in an external system such
// as a cloud provider API. Each controller must watch the managed resource kind
// for which it is responsible.
type Reconciler struct {
client client.Client
newManaged func() resource.Managed
pollInterval time.Duration
pollIntervalHook PollIntervalHook
timeout time.Duration
creationGracePeriod time.Duration
features feature.Flags
// The below structs embed the set of interfaces used to implement the
// managed resource reconciler. We do this primarily for readability, so
// that the reconciler logic reads r.external.Connect(),
// r.managed.Delete(), etc.
external mrExternal
managed mrManaged
supportedManagementPolicies []sets.Set[xpv1.ManagementAction]
log logging.Logger
record event.Recorder
metricRecorder MetricRecorder
change ChangeLogger
}
type mrManaged struct {
CriticalAnnotationUpdater
ConnectionPublisher
resource.Finalizer
Initializer
ReferenceResolver
}
func defaultMRManaged(m manager.Manager) mrManaged {
return mrManaged{
CriticalAnnotationUpdater: NewRetryingCriticalAnnotationUpdater(m.GetClient()),
Finalizer: resource.NewAPIFinalizer(m.GetClient(), FinalizerName),
Initializer: NewNameAsExternalName(m.GetClient()),
ReferenceResolver: NewAPISimpleReferenceResolver(m.GetClient()),
ConnectionPublisher: PublisherChain([]ConnectionPublisher{
NewAPISecretPublisher(m.GetClient(), m.GetScheme()),
&DisabledSecretStoreManager{},
}),
}
}
type mrExternal struct {
ExternalConnectDisconnecter
}
func defaultMRExternal() mrExternal {
return mrExternal{
ExternalConnectDisconnecter: NewNopDisconnecter(&NopConnecter{}),
}
}
// A ReconcilerOption configures a Reconciler.
type ReconcilerOption func(*Reconciler)
// WithTimeout specifies the timeout duration cumulatively for all the calls happen
// in the reconciliation function. In case the deadline exceeds, reconciler will
// still have some time to make the necessary calls to report the error such as
// status update.
func WithTimeout(duration time.Duration) ReconcilerOption {
return func(r *Reconciler) {
r.timeout = duration
}
}
// WithPollInterval specifies how long the Reconciler should wait before queueing
// a new reconciliation after a successful reconcile. The Reconciler requeues
// after a specified duration when it is not actively waiting for an external
// operation, but wishes to check whether an existing external resource needs to
// be synced to its Crossplane Managed resource.
func WithPollInterval(after time.Duration) ReconcilerOption {
return func(r *Reconciler) {
r.pollInterval = after
}
}
// WithMetricRecorder configures the Reconciler to use the supplied MetricRecorder.
func WithMetricRecorder(recorder MetricRecorder) ReconcilerOption {
return func(r *Reconciler) {
r.metricRecorder = recorder
}
}
// PollIntervalHook represents the function type passed to the
// WithPollIntervalHook option to support dynamic computation of the poll
// interval.
type PollIntervalHook func(managed resource.Managed, pollInterval time.Duration) time.Duration
func defaultPollIntervalHook(_ resource.Managed, pollInterval time.Duration) time.Duration {
return pollInterval
}
// WithPollIntervalHook adds a hook that can be used to configure the
// delay before an up-to-date resource is reconciled again after a successful
// reconcile. If this option is passed multiple times, only the latest hook
// will be used.
func WithPollIntervalHook(hook PollIntervalHook) ReconcilerOption {
return func(r *Reconciler) {
r.pollIntervalHook = hook
}
}
// WithPollJitterHook adds a simple PollIntervalHook to add jitter to the poll
// interval used when queuing a new reconciliation after a successful
// reconcile. The added jitter will be a random duration between -jitter and
// +jitter. This option wraps WithPollIntervalHook, and is subject to the same
// constraint that only the latest hook will be used.
func WithPollJitterHook(jitter time.Duration) ReconcilerOption {
return WithPollIntervalHook(func(_ resource.Managed, pollInterval time.Duration) time.Duration {
return pollInterval + time.Duration((rand.Float64()-0.5)*2*float64(jitter)) //nolint:gosec // No need for secure randomness.
})
}
// WithCreationGracePeriod configures an optional period during which we will
// wait for the external API to report that a newly created external resource
// exists. This allows us to tolerate eventually consistent APIs that do not
// immediately report that newly created resources exist when queried. All
// resources have a 30 second grace period by default.
func WithCreationGracePeriod(d time.Duration) ReconcilerOption {
return func(r *Reconciler) {
r.creationGracePeriod = d
}
}
// WithExternalConnecter specifies how the Reconciler should connect to the API
// used to sync and delete external resources.
func WithExternalConnecter(c ExternalConnecter) ReconcilerOption {
return func(r *Reconciler) {
r.external.ExternalConnectDisconnecter = NewNopDisconnecter(c)
}
}
// WithTypedExternalConnector specifies how the Reconciler should connect to the API
// used to sync and delete external resources.
func WithTypedExternalConnector[managed resource.Managed](c TypedExternalConnecter[managed]) ReconcilerOption {
return func(r *Reconciler) {
r.external.ExternalConnectDisconnecter = &typedExternalConnectDisconnecterWrapper[managed]{
c: NewTypedNopDisconnecter(c),
}
}
}
// WithExternalConnectDisconnecter specifies how the Reconciler should connect and disconnect to the API
// used to sync and delete external resources.
//
// Deprecated: Please use Disconnect() on the ExternalClient for disconnecting from the provider.
func WithExternalConnectDisconnecter(c ExternalConnectDisconnecter) ReconcilerOption {
return func(r *Reconciler) {
r.external.ExternalConnectDisconnecter = c
}
}
// WithTypedExternalConnectDisconnecter specifies how the Reconciler should connect and disconnect to the API
// used to sync and delete external resources.
//
// Deprecated: Please use Disconnect() on the ExternalClient for disconnecting from the provider.
func WithTypedExternalConnectDisconnecter[managed resource.Managed](c TypedExternalConnectDisconnecter[managed]) ReconcilerOption {
return func(r *Reconciler) {
r.external.ExternalConnectDisconnecter = &typedExternalConnectDisconnecterWrapper[managed]{c}
}
}
// WithCriticalAnnotationUpdater specifies how the Reconciler should update a
// managed resource's critical annotations. Implementations typically contain
// some kind of retry logic to increase the likelihood that critical annotations
// (like non-deterministic external names) will be persisted.
func WithCriticalAnnotationUpdater(u CriticalAnnotationUpdater) ReconcilerOption {
return func(r *Reconciler) {
r.managed.CriticalAnnotationUpdater = u
}
}
// WithConnectionPublishers specifies how the Reconciler should publish
// its connection details such as credentials and endpoints.
func WithConnectionPublishers(p ...ConnectionPublisher) ReconcilerOption {
return func(r *Reconciler) {
r.managed.ConnectionPublisher = PublisherChain(p)
}
}
// WithInitializers specifies how the Reconciler should initialize a
// managed resource before calling any of the ExternalClient functions.
func WithInitializers(i ...Initializer) ReconcilerOption {
return func(r *Reconciler) {
r.managed.Initializer = InitializerChain(i)
}
}
// WithFinalizer specifies how the Reconciler should add and remove
// finalizers to and from the managed resource.
func WithFinalizer(f resource.Finalizer) ReconcilerOption {
return func(r *Reconciler) {
r.managed.Finalizer = f
}
}
// WithReferenceResolver specifies how the Reconciler should resolve any
// inter-resource references it encounters while reconciling managed resources.
func WithReferenceResolver(rr ReferenceResolver) ReconcilerOption {
return func(r *Reconciler) {
r.managed.ReferenceResolver = rr
}
}
// WithLogger specifies how the Reconciler should log messages.
func WithLogger(l logging.Logger) ReconcilerOption {
return func(r *Reconciler) {
r.log = l
}
}
// WithRecorder specifies how the Reconciler should record events.
func WithRecorder(er event.Recorder) ReconcilerOption {
return func(r *Reconciler) {
r.record = er
}
}
// WithManagementPolicies enables support for management policies.
func WithManagementPolicies() ReconcilerOption {
return func(r *Reconciler) {
r.features.Enable(feature.EnableBetaManagementPolicies)
}
}
// WithReconcilerSupportedManagementPolicies configures which management policies are
// supported by the reconciler.
func WithReconcilerSupportedManagementPolicies(supported []sets.Set[xpv1.ManagementAction]) ReconcilerOption {
return func(r *Reconciler) {
r.supportedManagementPolicies = supported
}
}
// WithChangeLogger enables support for capturing change logs during
// reconciliation.
func WithChangeLogger(c ChangeLogger) ReconcilerOption {
return func(r *Reconciler) {
r.change = c
}
}
// NewReconciler returns a Reconciler that reconciles managed resources of the
// supplied ManagedKind with resources in an external system such as a cloud
// provider API. It panics if asked to reconcile a managed resource kind that is
// not registered with the supplied manager's runtime.Scheme. The returned
// Reconciler reconciles with a dummy, no-op 'external system' by default;
// callers should supply an ExternalConnector that returns an ExternalClient
// capable of managing resources in a real system.
func NewReconciler(m manager.Manager, of resource.ManagedKind, o ...ReconcilerOption) *Reconciler {
nm := func() resource.Managed {
//nolint:forcetypeassert // If this isn't an MR it's a programming error and we want to panic.
return resource.MustCreateObject(schema.GroupVersionKind(of), m.GetScheme()).(resource.Managed)
}
// Panic early if we've been asked to reconcile a resource kind that has not
// been registered with our controller manager's scheme.
_ = nm()
r := &Reconciler{
client: m.GetClient(),
newManaged: nm,
pollInterval: defaultPollInterval,
pollIntervalHook: defaultPollIntervalHook,
creationGracePeriod: defaultGracePeriod,
timeout: reconcileTimeout,
managed: defaultMRManaged(m),
external: defaultMRExternal(),
supportedManagementPolicies: defaultSupportedManagementPolicies(),
log: logging.NewNopLogger(),
record: event.NewNopRecorder(),
metricRecorder: NewNopMetricRecorder(),
change: newNopChangeLogger(),
}
for _, ro := range o {
ro(r)
}
return r
}
// Reconcile a managed resource with an external resource.
func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (result reconcile.Result, err error) { //nolint:gocognit // See note below.
// NOTE(negz): This method is a well over our cyclomatic complexity goal.
// Be wary of adding additional complexity.
defer func() { result, err = errors.SilentlyRequeueOnConflict(result, err) }()
log := r.log.WithValues("request", req)
log.Debug("Reconciling")
ctx, cancel := context.WithTimeout(ctx, r.timeout+reconcileGracePeriod)
defer cancel()
externalCtx, externalCancel := context.WithTimeout(ctx, r.timeout)
defer externalCancel()
managed := r.newManaged()
if err := r.client.Get(ctx, req.NamespacedName, managed); err != nil {
// There's no need to requeue if we no longer exist. Otherwise we'll be
// requeued implicitly because we return an error.
log.Debug("Cannot get managed resource", "error", err)
return reconcile.Result{}, errors.Wrap(resource.IgnoreNotFound(err), errGetManaged)
}
r.metricRecorder.recordFirstTimeReconciled(managed)
record := r.record.WithAnnotations("external-name", meta.GetExternalName(managed))
log = log.WithValues(
"uid", managed.GetUID(),
"version", managed.GetResourceVersion(),
"external-name", meta.GetExternalName(managed),
)
managementPoliciesEnabled := r.features.Enabled(feature.EnableBetaManagementPolicies)
if managementPoliciesEnabled {
log.WithValues("managementPolicies", managed.GetManagementPolicies())
}
// Create the management policy resolver which will assist us in determining
// what actions to take on the managed resource based on the management
// and deletion policies.
policy := NewManagementPoliciesResolver(managementPoliciesEnabled, managed.GetManagementPolicies(), managed.GetDeletionPolicy(), WithSupportedManagementPolicies(r.supportedManagementPolicies))
// Check if the resource has paused reconciliation based on the
// annotation or the management policies.
// Log, publish an event and update the SYNC status condition.
if meta.IsPaused(managed) || policy.IsPaused() {
log.Debug("Reconciliation is paused either through the `spec.managementPolicies` or the pause annotation", "annotation", meta.AnnotationKeyReconciliationPaused)
record.Event(managed, event.Normal(reasonReconciliationPaused, "Reconciliation is paused either through the `spec.managementPolicies` or the pause annotation",
"annotation", meta.AnnotationKeyReconciliationPaused))
managed.SetConditions(xpv1.ReconcilePaused())
// if the pause annotation is removed or the management policies changed, we will have a chance to reconcile
// again and resume and if status update fails, we will reconcile again to retry to update the status
return reconcile.Result{}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
// Check if the ManagementPolicies is set to a non-default value while the
// feature is not enabled. This is a safety check to let users know that
// they need to enable the feature flag before using the feature. For
// example, we wouldn't want someone to set the policy to ObserveOnly but
// not realize that the controller is still trying to reconcile
// (and modify or delete) the resource since they forgot to enable the
// feature flag. Also checks if the management policy is set to a value
// that is not supported by the controller.
if err := policy.Validate(); err != nil {
log.Debug(err.Error())
if kerrors.IsConflict(err) {
return reconcile.Result{Requeue: true}, nil
}
record.Event(managed, event.Warning(reasonManagementPolicyInvalid, err))
managed.SetConditions(xpv1.ReconcileError(err))
return reconcile.Result{}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
// If managed resource has a deletion timestamp and a deletion policy of
// Orphan, we do not need to observe the external resource before attempting
// to unpublish connection details and remove finalizer.
if meta.WasDeleted(managed) && !policy.ShouldDelete() {
log = log.WithValues("deletion-timestamp", managed.GetDeletionTimestamp())
// Empty ConnectionDetails are passed to UnpublishConnection because we
// have not retrieved them from the external resource. In practice we
// currently only write connection details to a Secret, and we rely on
// garbage collection to delete the entire secret, regardless of the
// supplied connection details.
if err := r.managed.UnpublishConnection(ctx, managed, ConnectionDetails{}); err != nil {
// If this is the first time we encounter this issue we'll be
// requeued implicitly when we update our status with the new error
// condition. If not, we requeue explicitly, which will trigger
// backoff.
log.Debug("Cannot unpublish connection details", "error", err)
if kerrors.IsConflict(err) {
return reconcile.Result{Requeue: true}, nil
}
record.Event(managed, event.Warning(reasonCannotUnpublish, err))
managed.SetConditions(xpv1.Deleting(), xpv1.ReconcileError(err))
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
if err := r.managed.RemoveFinalizer(ctx, managed); err != nil {
// If this is the first time we encounter this issue we'll be
// requeued implicitly when we update our status with the new error
// condition. If not, we requeue explicitly, which will trigger
// backoff.
log.Debug("Cannot remove managed resource finalizer", "error", err)
if kerrors.IsConflict(err) {
return reconcile.Result{Requeue: true}, nil
}
managed.SetConditions(xpv1.Deleting(), xpv1.ReconcileError(err))
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
// We've successfully unpublished our managed resource's connection
// details and removed our finalizer. If we assume we were the only
// controller that added a finalizer to this resource then it should no
// longer exist and thus there is no point trying to update its status.
r.metricRecorder.recordDeleted(managed)
log.Debug("Successfully deleted managed resource")
return reconcile.Result{Requeue: false}, nil
}
if err := r.managed.Initialize(ctx, managed); err != nil {
// If this is the first time we encounter this issue we'll be requeued
// implicitly when we update our status with the new error condition. If
// not, we requeue explicitly, which will trigger backoff.
log.Debug("Cannot initialize managed resource", "error", err)
if kerrors.IsConflict(err) {
return reconcile.Result{Requeue: true}, nil
}
record.Event(managed, event.Warning(reasonCannotInitialize, err))
managed.SetConditions(xpv1.ReconcileError(err))
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
// If we started but never completed creation of an external resource we
// may have lost critical information. For example if we didn't persist
// an updated external name we've leaked a resource. The safest thing to
// do is to refuse to proceed.
if meta.ExternalCreateIncomplete(managed) {
log.Debug(errCreateIncomplete)
record.Event(managed, event.Warning(reasonCannotInitialize, errors.New(errCreateIncomplete)))
managed.SetConditions(xpv1.Creating(), xpv1.ReconcileError(errors.New(errCreateIncomplete)))
return reconcile.Result{Requeue: false}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
// We resolve any references before observing our external resource because
// in some rare examples we need a spec field to make the observe call, and
// that spec field could be set by a reference.
//
// We do not resolve references when being deleted because it is likely that
// the resources we reference are also being deleted, and would thus block
// resolution due to being unready or non-existent. It is unlikely (but not
// impossible) that we need to resolve a reference in order to process a
// delete, and that reference is stale at delete time.
if !meta.WasDeleted(managed) {
if err := r.managed.ResolveReferences(ctx, managed); err != nil {
// If any of our referenced resources are not yet ready (or if we
// encountered an error resolving them) we want to try again. If
// this is the first time we encounter this situation we'll be
// requeued implicitly due to the status update. If not, we want
// requeue explicitly, which will trigger backoff.
log.Debug("Cannot resolve managed resource references", "error", err)
if kerrors.IsConflict(err) {
return reconcile.Result{Requeue: true}, nil
}
record.Event(managed, event.Warning(reasonCannotResolveRefs, err))
managed.SetConditions(xpv1.ReconcileError(err))
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}