Skip to content

Commit 2058167

Browse files
authored
Manage resource deletion and event filtering, fix imports (#40)
* debug: sync period * remove references to old fork * path eventFiltering OK, policy deletion OK Left to do : apply eventFiltering and resource deletion for the other two resources, and test behavior * Level resource deletion and event filtering across all three resources
1 parent d45440e commit 2058167

File tree

13 files changed

+422
-101
lines changed

13 files changed

+422
-101
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ Dockerfile.cross
2424
*.swp
2525
*.swo
2626
*~
27+
.vscode

PROJECT

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ plugins:
99
manifests.sdk.operatorframework.io/v2: {}
1010
scorecard.sdk.operatorframework.io/v2: {}
1111
projectName: s3-operator
12-
repo: github.com/phlg/s3-operator-downgrade
12+
repo: github.com/InseeFrLab/s3-operator
1313
resources:
1414
- api:
1515
crdVersion: v1
@@ -18,7 +18,7 @@ resources:
1818
domain: onyxia.sh
1919
group: s3.onyxia.sh
2020
kind: Bucket
21-
path: github.com/phlg/s3-operator-downgrade/api/v1alpha1
21+
path: github.com/InseeFrLab/s3-operator/api/v1alpha1
2222
version: v1alpha1
2323
- api:
2424
crdVersion: v1
@@ -27,7 +27,7 @@ resources:
2727
domain: onyxia.sh
2828
group: s3.onyxia.sh
2929
kind: Policy
30-
path: github.com/phlg/s3-operator-downgrade/api/v1alpha1
30+
path: github.com/InseeFrLab/s3-operator/api/v1alpha1
3131
version: v1alpha1
3232
- api:
3333
crdVersion: v1
@@ -36,6 +36,6 @@ resources:
3636
domain: onyxia.sh
3737
group: s3.onyxia.sh
3838
kind: Path
39-
path: github.com/phlg/s3-operator-downgrade/api/v1alpha1
39+
path: github.com/InseeFrLab/s3-operator/api/v1alpha1
4040
version: v1alpha1
4141
version: "3"

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,10 @@ The parameters are summarized in the table below :
8282
| `s3-endpoint-url` | `localhost:9000` | - | no | Hostname (or hostname:port) of the S3 server.
8383
| `s3-provider` | `minio` | - | no | S3 provider (possible values : `minio`, `mockedS3Provider`)
8484
| `s3-secret-key` | - | `S3_SECRET_KEY` | no | The secret key used to interact with the S3 server.
85-
| `useSsl` | `true` | - | no | Use of SSL/TLS to connect to the S3 server
85+
| `useSsl` | true | - | no | Use of SSL/TLS to connect to the S3 server
86+
| `bucket-deletion` | false | - | no | Trigger bucket deletion on the S3 backend upon CR deletion. Will fail if bucket is not empty.
87+
| `policy-deletion` | false | - | no | Trigger policy deletion on the S3 backend upon CR deletion
88+
| `path-deletion` | false | - | no | Trigger path deletion on the S3 backend upon CR deletion. Limited to deleting the `.keep` files used by the operator.
8689

8790

8891
## Usage

api/v1alpha1/path_types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ type PathSpec struct {
3939

4040
// PathStatus defines the observed state of Path
4141
type PathStatus struct {
42-
// Status management using Conditions.
42+
// Status management using Conditions.
4343
// See also : https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties
4444
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
4545
}

controllers/bucket_controller.go

Lines changed: 118 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ package controllers
1919
import (
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
4141
type 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/controller-runtime@v0.14.1/pkg/reconcile
5659
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")
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.*
172221
func (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+
191274
func (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/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())
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

Comments
 (0)