@@ -19,7 +19,6 @@ package controllers
19
19
import (
20
20
"context"
21
21
"fmt"
22
- "time"
23
22
24
23
"k8s.io/apimachinery/pkg/api/errors"
25
24
"k8s.io/apimachinery/pkg/api/meta"
@@ -29,47 +28,97 @@ import (
29
28
ctrl "sigs.k8s.io/controller-runtime"
30
29
"sigs.k8s.io/controller-runtime/pkg/client"
31
30
"sigs.k8s.io/controller-runtime/pkg/controller"
31
+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
32
32
"sigs.k8s.io/controller-runtime/pkg/event"
33
33
"sigs.k8s.io/controller-runtime/pkg/log"
34
34
"sigs.k8s.io/controller-runtime/pkg/predicate"
35
35
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"
38
38
)
39
39
40
40
// BucketReconciler reconciles a Bucket object
41
41
type BucketReconciler struct {
42
42
client.Client
43
- Scheme * runtime.Scheme
44
- S3Client factory.S3Client
43
+ Scheme * runtime.Scheme
44
+ S3Client factory.S3Client
45
+ BucketDeletion bool
45
46
}
46
47
47
48
//+kubebuilder:rbac:groups=s3.onyxia.sh,resources=buckets,verbs=get;list;watch;create;update;patch;delete
48
49
//+kubebuilder:rbac:groups=s3.onyxia.sh,resources=buckets/status,verbs=get;update;patch
49
50
//+kubebuilder:rbac:groups=s3.onyxia.sh,resources=buckets/finalizers,verbs=update
50
51
52
+ const bucketFinalizer = "s3.onyxia.sh/finalizer"
53
+
51
54
// Reconcile is part of the main kubernetes reconciliation loop which aims to
52
55
// move the current state of the cluster closer to the desired state.
53
56
//
54
57
// For more details, check Reconcile and its Result here:
55
58
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.1/pkg/reconcile
56
59
func (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" )
58
62
63
+ // Checking for bucket resource existence
59
64
bucketResource := & s3v1alpha1.Bucket {}
60
65
err := r .Get (ctx , req .NamespacedName , bucketResource )
61
66
if err != nil {
62
67
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 )
64
69
return ctrl.Result {}, nil
65
70
}
71
+ errorLogger .Error (err , "An error occurred when attempting to read the Bucket resource from the Kubernetes cluster" )
66
72
return ctrl.Result {}, err
67
73
}
68
74
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
+
69
118
// Check bucket existence on the S3 server
70
119
found , err := r .S3Client .BucketExists (bucketResource .Spec .Name )
71
120
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 )
73
122
return r .SetBucketStatusConditionAndUpdate (ctx , bucketResource , "OperatorFailed" , metav1 .ConditionFalse , "BucketExistenceCheckFailed" ,
74
123
fmt .Sprintf ("Checking existence of bucket [%s] from S3 instance has failed" , bucketResource .Spec .Name ), err )
75
124
}
@@ -80,24 +129,24 @@ func (r *BucketReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
80
129
// Bucket creation
81
130
err = r .S3Client .CreateBucket (bucketResource .Spec .Name )
82
131
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 )
84
133
return r .SetBucketStatusConditionAndUpdate (ctx , bucketResource , "OperatorFailed" , metav1 .ConditionFalse , "BucketCreationFailed" ,
85
134
fmt .Sprintf ("Creation of bucket [%s] on S3 instance has failed" , bucketResource .Spec .Name ), err )
86
135
}
87
136
88
137
// Setting quotas
89
138
err = r .S3Client .SetQuota (bucketResource .Spec .Name , bucketResource .Spec .Quota .Default )
90
139
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 )
92
141
return r .SetBucketStatusConditionAndUpdate (ctx , bucketResource , "OperatorFailed" , metav1 .ConditionFalse , "SetQuotaOnBucketFailed" ,
93
142
fmt .Sprintf ("Setting a quota of [%v] on bucket [%s] has failed" , bucketResource .Spec .Quota .Default , bucketResource .Spec .Name ), err )
94
143
}
95
144
96
- // Création des chemins
145
+ // Path creation
97
146
for _ , v := range bucketResource .Spec .Paths {
98
147
err = r .S3Client .CreatePath (bucketResource .Spec .Name , v )
99
148
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 )
101
150
return r .SetBucketStatusConditionAndUpdate (ctx , bucketResource , "OperatorFailed" , metav1 .ConditionFalse , "CreatingPathOnBucketFailed" ,
102
151
fmt .Sprintf ("Creating the path [%s] on bucket [%s] has failed" , v , bucketResource .Spec .Name ), err )
103
152
}
@@ -114,7 +163,7 @@ func (r *BucketReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
114
163
// Checking effectiveQuota existence on the bucket
115
164
effectiveQuota , err := r .S3Client .GetQuota (bucketResource .Spec .Name )
116
165
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 )
118
167
return r .SetBucketStatusConditionAndUpdate (ctx , bucketResource , "OperatorFailed" , metav1 .ConditionFalse , "BucketQuotaCheckFailed" ,
119
168
fmt .Sprintf ("The check for a quota on bucket [%s] has failed" , bucketResource .Spec .Name ), err )
120
169
}
@@ -131,7 +180,7 @@ func (r *BucketReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
131
180
if effectiveQuota != quotaToResetTo {
132
181
err = r .S3Client .SetQuota (bucketResource .Spec .Name , quotaToResetTo )
133
182
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 )
135
184
return r .SetBucketStatusConditionAndUpdate (ctx , bucketResource , "OperatorFailed" , metav1 .ConditionFalse , "BucketQuotaUpdateFailed" ,
136
185
fmt .Sprintf ("The quota update (%v => %v) on bucket [%s] has failed" , effectiveQuota , quotaToResetTo , bucketResource .Spec .Name ), err )
137
186
}
@@ -147,15 +196,15 @@ func (r *BucketReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
147
196
for _ , pathInCr := range bucketResource .Spec .Paths {
148
197
pathExists , err := r .S3Client .PathExists (bucketResource .Spec .Name , pathInCr )
149
198
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 )
151
200
return r .SetBucketStatusConditionAndUpdate (ctx , bucketResource , "OperatorFailed" , metav1 .ConditionFalse , "BucketPathCheckFailed" ,
152
201
fmt .Sprintf ("The check for path [%s] on bucket [%s] has failed" , pathInCr , bucketResource .Spec .Name ), err )
153
202
}
154
203
155
204
if ! pathExists {
156
205
err = r .S3Client .CreatePath (bucketResource .Spec .Name , pathInCr )
157
206
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 )
159
208
return r .SetBucketStatusConditionAndUpdate (ctx , bucketResource , "OperatorFailed" , metav1 .ConditionFalse , "BucketPathCreationFailed" ,
160
209
fmt .Sprintf ("The creation of path [%s] on bucket [%s] has failed" , pathInCr , bucketResource .Spec .Name ), err )
161
210
}
@@ -170,33 +219,78 @@ func (r *BucketReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
170
219
171
220
// SetupWithManager sets up the controller with the Manager.*
172
221
func (r * BucketReconciler ) SetupWithManager (mgr ctrl.Manager ) error {
222
+ logger := ctrl .Log .WithName ("bucketEventFilter" )
173
223
return ctrl .NewControllerManagedBy (mgr ).
174
224
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/
177
226
WithEventFilter (predicate.Funcs {
178
227
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
181
256
},
182
257
DeleteFunc : func (e event.DeleteEvent ) bool {
183
258
// Evaluates to false if the object has been confirmed deleted.
259
+ logger .Info ("reconcile delete event is filtered out" , "resource" , e .Object .GetName ())
184
260
return ! e .DeleteStateUnknown
185
261
},
186
262
}).
187
263
WithOptions (controller.Options {MaxConcurrentReconciles : 10 }).
188
264
Complete (r )
189
265
}
190
266
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
+
191
274
func (r * BucketReconciler ) SetBucketStatusConditionAndUpdate (ctx context.Context , bucketResource * s3v1alpha1.Bucket , conditionType string , status metav1.ConditionStatus , reason string , message string , srcError error ) (ctrl.Result , error ) {
192
275
logger := log .FromContext (ctx )
193
276
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/apimachinery@v0.28.3/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())
194
288
meta .SetStatusCondition (& bucketResource .Status .Conditions ,
195
289
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()),
200
294
Message : message ,
201
295
ObservedGeneration : bucketResource .GetGeneration (),
202
296
})
0 commit comments