@@ -19,7 +19,6 @@ package controllers
1919import (
2020 "context"
2121 "fmt"
22- "time"
2322
2423 "k8s.io/apimachinery/pkg/api/errors"
2524 "k8s.io/apimachinery/pkg/api/meta"
@@ -29,47 +28,97 @@ import (
2928 ctrl "sigs.k8s.io/controller-runtime"
3029 "sigs.k8s.io/controller-runtime/pkg/client"
3130 "sigs.k8s.io/controller-runtime/pkg/controller"
31+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
3232 "sigs.k8s.io/controller-runtime/pkg/event"
3333 "sigs.k8s.io/controller-runtime/pkg/log"
3434 "sigs.k8s.io/controller-runtime/pkg/predicate"
3535
36- s3v1alpha1 "github.com/phlg /s3-operator-downgrade /api/v1alpha1"
37- "github.com/phlg /s3-operator-downgrade /controllers/s3/factory"
36+ s3v1alpha1 "github.com/InseeFrLab /s3-operator/api/v1alpha1"
37+ "github.com/InseeFrLab /s3-operator/controllers/s3/factory"
3838)
3939
4040// BucketReconciler reconciles a Bucket object
4141type BucketReconciler struct {
4242 client.Client
43- Scheme * runtime.Scheme
44- S3Client factory.S3Client
43+ Scheme * runtime.Scheme
44+ S3Client factory.S3Client
45+ BucketDeletion bool
4546}
4647
4748//+kubebuilder:rbac:groups=s3.onyxia.sh,resources=buckets,verbs=get;list;watch;create;update;patch;delete
4849//+kubebuilder:rbac:groups=s3.onyxia.sh,resources=buckets/status,verbs=get;update;patch
4950//+kubebuilder:rbac:groups=s3.onyxia.sh,resources=buckets/finalizers,verbs=update
5051
52+ const bucketFinalizer = "s3.onyxia.sh/finalizer"
53+
5154// Reconcile is part of the main kubernetes reconciliation loop which aims to
5255// move the current state of the cluster closer to the desired state.
5356//
5457// For more details, check Reconcile and its Result here:
5558// - https://pkg.go.dev/sigs.k8s.io/[email protected] /pkg/reconcile 5659func (r * BucketReconciler ) Reconcile (ctx context.Context , req ctrl.Request ) (ctrl.Result , error ) {
57- logger := log .FromContext (ctx )
60+ errorLogger := log .FromContext (ctx )
61+ logger := ctrl .Log .WithName ("bucketReconcile" )
5862
63+ // Checking for bucket resource existence
5964 bucketResource := & s3v1alpha1.Bucket {}
6065 err := r .Get (ctx , req .NamespacedName , bucketResource )
6166 if err != nil {
6267 if errors .IsNotFound (err ) {
63- logger .Info (fmt . Sprintf ( " Bucket CRD %s has been removed. NOOP" , req .Name ) )
68+ logger .Info ("The Bucket custom resource has been removed ; as such the Bucket controller is NOOP. " , " req.Name" , req . Name )
6469 return ctrl.Result {}, nil
6570 }
71+ errorLogger .Error (err , "An error occurred when attempting to read the Bucket resource from the Kubernetes cluster" )
6672 return ctrl.Result {}, err
6773 }
6874
75+ // Managing bucket deletion with a finalizer
76+ // REF : https://sdk.operatorframework.io/docs/building-operators/golang/advanced-topics/#external-resources
77+ isMarkedForDeletion := bucketResource .GetDeletionTimestamp () != nil
78+ if isMarkedForDeletion {
79+ if controllerutil .ContainsFinalizer (bucketResource , bucketFinalizer ) {
80+ // Run finalization logic for bucketFinalizer. If the
81+ // finalization logic fails, don't remove the finalizer so
82+ // that we can retry during the next reconciliation.
83+ if err := r .finalizeBucket (bucketResource ); err != nil {
84+ // return ctrl.Result{}, err
85+ errorLogger .Error (err , "an error occurred when attempting to finalize the bucket" , "bucket" , bucketResource .Spec .Name )
86+ // return ctrl.Result{}, err
87+ return r .SetBucketStatusConditionAndUpdate (ctx , bucketResource , "OperatorFailed" , metav1 .ConditionFalse , "BucketFinalizeFailed" ,
88+ fmt .Sprintf ("An error occurred when attempting to delete bucket [%s]" , bucketResource .Spec .Name ), err )
89+ }
90+
91+ // Remove bucketFinalizer. Once all finalizers have been
92+ // removed, the object will be deleted.
93+ controllerutil .RemoveFinalizer (bucketResource , bucketFinalizer )
94+ err := r .Update (ctx , bucketResource )
95+ if err != nil {
96+ errorLogger .Error (err , "an error occurred when removing finalizer from bucket" , "bucket" , bucketResource .Spec .Name )
97+ // return ctrl.Result{}, err
98+ return r .SetBucketStatusConditionAndUpdate (ctx , bucketResource , "OperatorFailed" , metav1 .ConditionFalse , "BucketFinalizerRemovalFailed" ,
99+ fmt .Sprintf ("An error occurred when attempting to remove the finalizer from bucket [%s]" , bucketResource .Spec .Name ), err )
100+ }
101+ }
102+ return ctrl.Result {}, nil
103+ }
104+
105+ // Add finalizer for this CR
106+ if ! controllerutil .ContainsFinalizer (bucketResource , bucketFinalizer ) {
107+ controllerutil .AddFinalizer (bucketResource , bucketFinalizer )
108+ err = r .Update (ctx , bucketResource )
109+ if err != nil {
110+ errorLogger .Error (err , "an error occurred when adding finalizer from bucket" , "bucket" , bucketResource .Spec .Name )
111+ return r .SetBucketStatusConditionAndUpdate (ctx , bucketResource , "OperatorFailed" , metav1 .ConditionFalse , "BucketFinalizerAddFailed" ,
112+ fmt .Sprintf ("An error occurred when attempting to add the finalizer from bucket [%s]" , bucketResource .Spec .Name ), err )
113+ }
114+ }
115+
116+ // Bucket lifecycle management (other than deletion) starts here
117+
69118 // Check bucket existence on the S3 server
70119 found , err := r .S3Client .BucketExists (bucketResource .Spec .Name )
71120 if err != nil {
72- logger .Error (err , "an error occurred while checking the existence of a bucket" , "bucket" , bucketResource .Spec .Name )
121+ errorLogger .Error (err , "an error occurred while checking the existence of a bucket" , "bucket" , bucketResource .Spec .Name )
73122 return r .SetBucketStatusConditionAndUpdate (ctx , bucketResource , "OperatorFailed" , metav1 .ConditionFalse , "BucketExistenceCheckFailed" ,
74123 fmt .Sprintf ("Checking existence of bucket [%s] from S3 instance has failed" , bucketResource .Spec .Name ), err )
75124 }
@@ -80,24 +129,24 @@ func (r *BucketReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
80129 // Bucket creation
81130 err = r .S3Client .CreateBucket (bucketResource .Spec .Name )
82131 if err != nil {
83- logger .Error (err , "an error occurred while creating a bucket" , "bucket" , bucketResource .Spec .Name )
132+ errorLogger .Error (err , "an error occurred while creating a bucket" , "bucket" , bucketResource .Spec .Name )
84133 return r .SetBucketStatusConditionAndUpdate (ctx , bucketResource , "OperatorFailed" , metav1 .ConditionFalse , "BucketCreationFailed" ,
85134 fmt .Sprintf ("Creation of bucket [%s] on S3 instance has failed" , bucketResource .Spec .Name ), err )
86135 }
87136
88137 // Setting quotas
89138 err = r .S3Client .SetQuota (bucketResource .Spec .Name , bucketResource .Spec .Quota .Default )
90139 if err != nil {
91- logger .Error (err , "an error occurred while setting a quota on a bucket" , "bucket" , bucketResource .Spec .Name , "quota" , bucketResource .Spec .Quota .Default )
140+ errorLogger .Error (err , "an error occurred while setting a quota on a bucket" , "bucket" , bucketResource .Spec .Name , "quota" , bucketResource .Spec .Quota .Default )
92141 return r .SetBucketStatusConditionAndUpdate (ctx , bucketResource , "OperatorFailed" , metav1 .ConditionFalse , "SetQuotaOnBucketFailed" ,
93142 fmt .Sprintf ("Setting a quota of [%v] on bucket [%s] has failed" , bucketResource .Spec .Quota .Default , bucketResource .Spec .Name ), err )
94143 }
95144
96- // Création des chemins
145+ // Path creation
97146 for _ , v := range bucketResource .Spec .Paths {
98147 err = r .S3Client .CreatePath (bucketResource .Spec .Name , v )
99148 if err != nil {
100- logger .Error (err , "an error occurred while creating a path on a bucket" , "bucket" , bucketResource .Spec .Name , "path" , v )
149+ errorLogger .Error (err , "an error occurred while creating a path on a bucket" , "bucket" , bucketResource .Spec .Name , "path" , v )
101150 return r .SetBucketStatusConditionAndUpdate (ctx , bucketResource , "OperatorFailed" , metav1 .ConditionFalse , "CreatingPathOnBucketFailed" ,
102151 fmt .Sprintf ("Creating the path [%s] on bucket [%s] has failed" , v , bucketResource .Spec .Name ), err )
103152 }
@@ -114,7 +163,7 @@ func (r *BucketReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
114163 // Checking effectiveQuota existence on the bucket
115164 effectiveQuota , err := r .S3Client .GetQuota (bucketResource .Spec .Name )
116165 if err != nil {
117- logger .Error (err , "an error occurred while getting the quota for a bucket" , "bucket" , bucketResource .Spec .Name )
166+ errorLogger .Error (err , "an error occurred while getting the quota for a bucket" , "bucket" , bucketResource .Spec .Name )
118167 return r .SetBucketStatusConditionAndUpdate (ctx , bucketResource , "OperatorFailed" , metav1 .ConditionFalse , "BucketQuotaCheckFailed" ,
119168 fmt .Sprintf ("The check for a quota on bucket [%s] has failed" , bucketResource .Spec .Name ), err )
120169 }
@@ -131,7 +180,7 @@ func (r *BucketReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
131180 if effectiveQuota != quotaToResetTo {
132181 err = r .S3Client .SetQuota (bucketResource .Spec .Name , quotaToResetTo )
133182 if err != nil {
134- logger .Error (err , "an error occurred while resetting the quota for a bucket" , "bucket" , bucketResource .Spec .Name , "quotaToResetTo" , quotaToResetTo )
183+ errorLogger .Error (err , "an error occurred while resetting the quota for a bucket" , "bucket" , bucketResource .Spec .Name , "quotaToResetTo" , quotaToResetTo )
135184 return r .SetBucketStatusConditionAndUpdate (ctx , bucketResource , "OperatorFailed" , metav1 .ConditionFalse , "BucketQuotaUpdateFailed" ,
136185 fmt .Sprintf ("The quota update (%v => %v) on bucket [%s] has failed" , effectiveQuota , quotaToResetTo , bucketResource .Spec .Name ), err )
137186 }
@@ -147,15 +196,15 @@ func (r *BucketReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
147196 for _ , pathInCr := range bucketResource .Spec .Paths {
148197 pathExists , err := r .S3Client .PathExists (bucketResource .Spec .Name , pathInCr )
149198 if err != nil {
150- logger .Error (err , "an error occurred while checking a path's existence on a bucket" , "bucket" , bucketResource .Spec .Name , "path" , pathInCr )
199+ errorLogger .Error (err , "an error occurred while checking a path's existence on a bucket" , "bucket" , bucketResource .Spec .Name , "path" , pathInCr )
151200 return r .SetBucketStatusConditionAndUpdate (ctx , bucketResource , "OperatorFailed" , metav1 .ConditionFalse , "BucketPathCheckFailed" ,
152201 fmt .Sprintf ("The check for path [%s] on bucket [%s] has failed" , pathInCr , bucketResource .Spec .Name ), err )
153202 }
154203
155204 if ! pathExists {
156205 err = r .S3Client .CreatePath (bucketResource .Spec .Name , pathInCr )
157206 if err != nil {
158- logger .Error (err , "an error occurred while creating a path on a bucket" , "bucket" , bucketResource .Spec .Name , "path" , pathInCr )
207+ errorLogger .Error (err , "an error occurred while creating a path on a bucket" , "bucket" , bucketResource .Spec .Name , "path" , pathInCr )
159208 return r .SetBucketStatusConditionAndUpdate (ctx , bucketResource , "OperatorFailed" , metav1 .ConditionFalse , "BucketPathCreationFailed" ,
160209 fmt .Sprintf ("The creation of path [%s] on bucket [%s] has failed" , pathInCr , bucketResource .Spec .Name ), err )
161210 }
@@ -170,33 +219,78 @@ func (r *BucketReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
170219
171220// SetupWithManager sets up the controller with the Manager.*
172221func (r * BucketReconciler ) SetupWithManager (mgr ctrl.Manager ) error {
222+ logger := ctrl .Log .WithName ("bucketEventFilter" )
173223 return ctrl .NewControllerManagedBy (mgr ).
174224 For (& s3v1alpha1.Bucket {}).
175- // TODO : implement a real strategy for event filtering ; for now just using the example from OpSDK doc
176- // (https://sdk.operatorframework.io/docs/building-operators/golang/references/event-filtering/)
225+ // REF : https://sdk.operatorframework.io/docs/building-operators/golang/references/event-filtering/
177226 WithEventFilter (predicate.Funcs {
178227 UpdateFunc : func (e event.UpdateEvent ) bool {
179- // Ignore updates to CR status in which case metadata.Generation does not change
180- return e .ObjectOld .GetGeneration () != e .ObjectNew .GetGeneration ()
228+ // Only reconcile if :
229+ // - Generation has changed
230+ // or
231+ // - Of all Conditions matching the last generation, none is in status "True"
232+ // There is an implicit assumption that in such a case, the resource was once failing, but then transitioned
233+ // to a functional state. We use this ersatz because lastTransitionTime appears to not work properly - see also
234+ // comment in SetBucketStatusConditionAndUpdate() below.
235+ newBucket , _ := e .ObjectNew .(* s3v1alpha1.Bucket )
236+
237+ // 1 - Identifying the most recent generation
238+ var maxGeneration int64 = 0
239+ for _ , condition := range newBucket .Status .Conditions {
240+ if condition .ObservedGeneration > maxGeneration {
241+ maxGeneration = condition .ObservedGeneration
242+ }
243+ }
244+ // 2 - Checking one of the conditions in most recent generation is True
245+ conditionTrueInLastGeneration := false
246+ for _ , condition := range newBucket .Status .Conditions {
247+ if condition .ObservedGeneration == maxGeneration && condition .Status == metav1 .ConditionTrue {
248+ conditionTrueInLastGeneration = true
249+ }
250+ }
251+ predicate := e .ObjectOld .GetGeneration () != e .ObjectNew .GetGeneration () || ! conditionTrueInLastGeneration
252+ if ! predicate {
253+ logger .Info ("reconcile update event is filtered out" , "resource" , e .ObjectNew .GetName ())
254+ }
255+ return predicate
181256 },
182257 DeleteFunc : func (e event.DeleteEvent ) bool {
183258 // Evaluates to false if the object has been confirmed deleted.
259+ logger .Info ("reconcile delete event is filtered out" , "resource" , e .Object .GetName ())
184260 return ! e .DeleteStateUnknown
185261 },
186262 }).
187263 WithOptions (controller.Options {MaxConcurrentReconciles : 10 }).
188264 Complete (r )
189265}
190266
267+ func (r * BucketReconciler ) finalizeBucket (bucketResource * s3v1alpha1.Bucket ) error {
268+ if r .BucketDeletion {
269+ return r .S3Client .DeleteBucket (bucketResource .Spec .Name )
270+ }
271+ return nil
272+ }
273+
191274func (r * BucketReconciler ) SetBucketStatusConditionAndUpdate (ctx context.Context , bucketResource * s3v1alpha1.Bucket , conditionType string , status metav1.ConditionStatus , reason string , message string , srcError error ) (ctrl.Result , error ) {
192275 logger := log .FromContext (ctx )
193276
277+ // It would seem LastTransitionTime does not work as intended (our understanding of the intent coming from this :
278+ // https://pkg.go.dev/k8s.io/[email protected] /pkg/api/meta#SetStatusCondition). Whether we set the 279+ // date manually or leave it out to have default behavior, the lastTransitionTime is NOT updated if the CR
280+ // had that condition at least once in the past.
281+ // For instance, with the following updates to a CR :
282+ // - gen 1 : condition type = A
283+ // - gen 2 : condition type = B
284+ // - gen 3 : condition type = A again
285+ // Then the condition with type A in CR Status will still have the lastTransitionTime dating back to gen 1.
286+ // Because of this, lastTransitionTime cannot be reliably used to determine current state, which in turn had
287+ // us turn to a less than ideal event filter (see above in SetupWithManager())
194288 meta .SetStatusCondition (& bucketResource .Status .Conditions ,
195289 metav1.Condition {
196- Type : conditionType ,
197- Status : status ,
198- Reason : reason ,
199- LastTransitionTime : metav1 .NewTime (time .Now ()),
290+ Type : conditionType ,
291+ Status : status ,
292+ Reason : reason ,
293+ // LastTransitionTime: metav1.NewTime(time.Now()),
200294 Message : message ,
201295 ObservedGeneration : bucketResource .GetGeneration (),
202296 })
0 commit comments