@@ -18,13 +18,22 @@ package reconciler
1818
1919import (
2020 "context"
21+ "fmt"
22+ "time"
2123
24+ "github.com/go-logr/logr"
25+ kerrors "k8s.io/apimachinery/pkg/api/errors"
2226 "k8s.io/apimachinery/pkg/runtime"
2327 ctrl "sigs.k8s.io/controller-runtime"
2428 "sigs.k8s.io/controller-runtime/pkg/client"
25- logf "sigs.k8s.io/controller-runtime/pkg/log"
29+ ctrlutil "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
30+ ctrlpredicate "sigs.k8s.io/controller-runtime/pkg/predicate"
31+ "sigs.k8s.io/controller-runtime/pkg/reconcile"
2632
33+ cosiapi "sigs.k8s.io/container-object-storage-interface/client/apis/objectstorage/v1alpha2"
2734 objectstoragev1alpha2 "sigs.k8s.io/container-object-storage-interface/client/apis/objectstorage/v1alpha2"
35+ "sigs.k8s.io/container-object-storage-interface/internal/handoff"
36+ cosipredicate "sigs.k8s.io/container-object-storage-interface/internal/predicate"
2837)
2938
3039// BucketAccessReconciler reconciles a BucketAccess object
@@ -33,31 +42,156 @@ type BucketAccessReconciler struct {
3342 Scheme * runtime.Scheme
3443}
3544
36- // +kubebuilder:rbac:groups=objectstorage.k8s.io,resources=bucketaccesses,verbs=get;list;watch;create;update;patch;delete
37- // +kubebuilder:rbac:groups=objectstorage.k8s.io,resources=bucketaccesses/status,verbs=get;update;patch
45+ // +kubebuilder:rbac:groups=objectstorage.k8s.io,resources=bucketaccesses,verbs=get;list;watch;create;update
46+ // +kubebuilder:rbac:groups=objectstorage.k8s.io,resources=bucketaccesses/status,verbs=get;update
3847// +kubebuilder:rbac:groups=objectstorage.k8s.io,resources=bucketaccesses/finalizers,verbs=update
3948
4049// Reconcile is part of the main kubernetes reconciliation loop which aims to
4150// move the current state of the cluster closer to the desired state.
42- // TODO(user): Modify the Reconcile function to compare the state specified by
43- // the BucketAccess object against the actual cluster state, and then
44- // perform operations to make the cluster state reflect the state specified by
45- // the user.
46- //
47- // For more details, check Reconcile and its Result here:
48- // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/reconcile
4951func (r * BucketAccessReconciler ) Reconcile (ctx context.Context , req ctrl.Request ) (ctrl.Result , error ) {
50- _ = logf . FromContext (ctx )
52+ logger := ctrl . LoggerFrom (ctx )
5153
52- // TODO(user): your logic here
54+ access := & cosiapi.BucketAccess {}
55+ if err := r .Get (ctx , req .NamespacedName , access ); err != nil {
56+ if kerrors .IsNotFound (err ) {
57+ logger .V (1 ).Info ("not reconciling nonexistent BucketAccess" )
58+ return ctrl.Result {}, nil
59+ }
60+ // no resource to add status to or report an event for
61+ logger .Error (err , "failed to get BucketAccess" )
62+ return ctrl.Result {}, err
63+ }
5364
54- return ctrl.Result {}, nil
65+ if handoff .BucketAccessManagedBySidecar (access ) {
66+ logger .V (1 ).Info ("not reconciling BucketAccess that should be managed by sidecar" )
67+ return ctrl.Result {}, nil
68+ }
69+
70+ retryError , err := r .reconcile (ctx , logger , access )
71+ if err != nil {
72+ // Because the BucketAccess status is could be managed by either Sidecar or Controller,
73+ // indicate that this error is coming from the Controller.
74+ err = fmt .Errorf ("COSI Controller error: %w" , err )
75+
76+ // Record any error as a timestamped error in the status.
77+ access .Status .Error = cosiapi .NewTimestampedError (time .Now (), err .Error ())
78+ if updErr := r .Status ().Update (ctx , access ); updErr != nil {
79+ logger .Error (err , "failed to update BucketAccess status after reconcile error" , "updateError" , updErr )
80+ // If status update fails, we must retry the error regardless of the reconcile return.
81+ // The reconcile needs to run again to make sure the status is eventually be updated.
82+ return reconcile.Result {}, err
83+ }
84+
85+ if ! retryError {
86+ return reconcile.Result {}, reconcile .TerminalError (err )
87+ }
88+ return reconcile.Result {}, err
89+ }
90+
91+ // NOTE: Do not clear the error in the status on success. Success indicates 1 of 2 things:
92+ // 1. BucketAccess was initialized successfully, and it's now owned by the Sidecar
93+ // 2. BucketAccess deletion cleanup was just finished, and no status update is needed
94+
95+ return reconcile.Result {}, err
5596}
5697
5798// SetupWithManager sets up the controller with the Manager.
5899func (r * BucketAccessReconciler ) SetupWithManager (mgr ctrl.Manager ) error {
59100 return ctrl .NewControllerManagedBy (mgr ).
60101 For (& objectstoragev1alpha2.BucketAccess {}).
61102 Named ("bucketaccess" ).
103+ WithEventFilter (
104+ ctrlpredicate .And (
105+ cosipredicate .BucketAccessManagedByController (r .Scheme ), // only opt in to reconciles managed by controller
106+ ctrlpredicate .Or (
107+ // when managed by controller, we should reconcile ALL Create/Delete/Generic events
108+ cosipredicate .AnyCreate (),
109+ cosipredicate .AnyDelete (),
110+ cosipredicate .AnyGeneric (),
111+ // opt in to desired update events
112+ cosipredicate .BucketAccessHandoffOccurred (r .Scheme ), // reconcile any handoff change
113+ cosipredicate .ProtectionFinalizerRemoved (r .Scheme ), // re-add protection finalizer if removed
114+ ),
115+ ),
116+ ).
62117 Complete (r )
63118}
119+
120+ func (r * BucketAccessReconciler ) reconcile (
121+ ctx context.Context , logger logr.Logger , access * cosiapi.BucketAccess ,
122+ ) (retryErrorType , error ) {
123+ if ! access .GetDeletionTimestamp ().IsZero () {
124+ logger .V (1 ).Info ("beginning BucketAccess deletion cleanup" )
125+
126+ // TODO: deletion logic
127+
128+ ctrlutil .RemoveFinalizer (access , cosiapi .ProtectionFinalizer )
129+ if err := r .Update (ctx , access ); err != nil {
130+ logger .Error (err , "failed to remove finalizer" )
131+ return RetryError , fmt .Errorf ("failed to remove finalizer: %w" , err )
132+ }
133+
134+ return DoNotRetryError , fmt .Errorf ("deletion is not yet implemented" ) // TODO
135+ }
136+
137+ needInit , err := readyForControllerInitialization (& access .Status )
138+ if err != nil {
139+ logger .Error (err , "processed a degraded BucketAccess" )
140+ return DoNotRetryError , fmt .Errorf ("processed a degraded BucketAccess: %w" , err )
141+ }
142+ if ! needInit {
143+ // BucketAccessClass info should only be copied to the BucketAccess status once, upon
144+ // initial provisioning. After the info is copied, we can make no attempt to fill in any
145+ // missing or lost info because we don't know whether the current Class is compatible with
146+ // the info from the existing (old) Class info. If we reach this condition, something is
147+ // systemically wrong. Sidecar should have ownership, but we determined otherwise, and the
148+ // Sidecar will likely also determine us to be the owner.
149+ logger .Error (nil , "processed a BucketAccess that should be managed by COSI Sidecar" )
150+ return DoNotRetryError , fmt .Errorf ("processed a BucketAccess that should be managed by COSI Sidecar" )
151+ }
152+
153+ // TODO:
154+ // 1. get referenced BucketClaims
155+ // a. for each, add HasBucketAccessReferencesAnnotation to the claim
156+ // b. for each, get Bucket info to build corresponding accessedBuckets list
157+ // 2. get BucketAccessClass
158+ // 3. build updated status
159+ // a. accessedBuckets list
160+ // b. driverName, authType, parameters
161+ // c. error = nil
162+ // 4. status().update()
163+
164+ return NoError , nil
165+ }
166+
167+ // Return true if the Controller needs to initialize the BucketAccess with BucketClaim and
168+ // BucketAccessClass info. Return false if required info is set.
169+ // Return an error if any required info is only partially set. This indicates some sort of
170+ // degradation or bug.
171+ func readyForControllerInitialization (s * cosiapi.BucketAccessStatus ) (bool , error ) {
172+ requiredFields := map [string ]bool {}
173+ requiredFieldIsSet := func (fieldName string , isSet bool ) {
174+ requiredFields [fieldName ] = isSet
175+ }
176+
177+ requiredFieldIsSet ("status.accessedBuckets" , len (s .AccessedBuckets ) > 0 )
178+ requiredFieldIsSet ("status.driverName" , s .DriverName != "" )
179+ requiredFieldIsSet ("status.authenticationType" , string (s .AuthenticationType ) != "" )
180+
181+ set := []string {}
182+ for field , isSet := range requiredFields {
183+ if isSet {
184+ set = append (set , field )
185+ }
186+ }
187+
188+ if len (set ) == 0 {
189+ return true , nil
190+ }
191+
192+ if len (set ) == len (requiredFields ) {
193+ return false , nil
194+ }
195+
196+ return false , fmt .Errorf ("required Controller-managed fields are only partially set: %v" , requiredFields )
197+ }
0 commit comments