• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DoodleScheduling / k8sgrowthbook-controller / 5474122465

06 Jul 2023 10:00AM UTC coverage: 74.169%. Remained the same
5474122465

push

github

web-flow
docs: remove invalid fields (#47)

692 of 933 relevant lines covered (74.17%)

31.17 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

69.95
/internal/controllers/instance_controller.go
1
/*
2

3

4
Licensed under the Apache License, Version 2.0 (the "License");
5
you may not use this file except in compliance with the License.
6
You may obtain a copy of the License at
7

8
    http://www.apache.org/licenses/LICENSE-2.0
9

10
Unless required by applicable law or agreed to in writing, software
11
distributed under the License is distributed on an "AS IS" BASIS,
12
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
See the License for the specific language governing permissions and
14
limitations under the License.
15
*/
16

17
package controllers
18

19
import (
20
        "context"
21
        "errors"
22
        "fmt"
23
        "net/url"
24
        "strings"
25
        "time"
26

27
        "github.com/go-logr/logr"
28
        "go.mongodb.org/mongo-driver/mongo"
29
        "go.mongodb.org/mongo-driver/mongo/options"
30
        "golang.org/x/exp/slices"
31
        corev1 "k8s.io/api/core/v1"
32
        apierrors "k8s.io/apimachinery/pkg/api/errors"
33
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
34
        "k8s.io/apimachinery/pkg/runtime"
35
        "k8s.io/apimachinery/pkg/types"
36
        "k8s.io/client-go/tools/record"
37
        ctrl "sigs.k8s.io/controller-runtime"
38
        "sigs.k8s.io/controller-runtime/pkg/builder"
39
        "sigs.k8s.io/controller-runtime/pkg/client"
40
        "sigs.k8s.io/controller-runtime/pkg/controller"
41
        "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
42
        "sigs.k8s.io/controller-runtime/pkg/handler"
43
        "sigs.k8s.io/controller-runtime/pkg/predicate"
44
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
45

46
        v1beta1 "github.com/DoodleScheduling/k8sgrowthbook-controller/api/v1beta1"
47
        "github.com/DoodleScheduling/k8sgrowthbook-controller/internal/growthbook"
48
        "github.com/DoodleScheduling/k8sgrowthbook-controller/internal/storage"
49
        "github.com/DoodleScheduling/k8sgrowthbook-controller/internal/storage/mongodb"
50
)
51

52
// +kubebuilder:rbac:groups=growthbook.infra.doodle.com,resources=growthbookinstances,verbs=get;list;watch;create;update;patch;delete
53
// +kubebuilder:rbac:groups=growthbook.infra.doodle.com,resources=growthbookinstances/status,verbs=get;update;patch
54
// +kubebuilder:rbac:groups=growthbook.infra.doodle.com,resources=growthbookorganizations,verbs=get;list;watch;create;update;patch;delete
55
// +kubebuilder:rbac:groups=growthbook.infra.doodle.com,resources=growthbookusers,verbs=get;list;watch;create;update;patch;delete
56
// +kubebuilder:rbac:groups=growthbook.infra.doodle.com,resources=growthbookfeatures,verbs=get;list;watch;create;update;patch;delete
57
// +kubebuilder:rbac:groups=growthbook.infra.doodle.com,resources=growthbookclients,verbs=get;list;watch;create;update;patch;delete
58
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
59
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
60

61
const (
62
        secretIndexKey = ".metadata.secret"
63
        usersIndexKey  = ".metadata.users"
64
        orgsIndexKey   = ".metadata.orgs"
65
        owner          = "k8sgrowthbook-controller"
66
)
67

68
// MongoDBProvider returns a storage.Database for MongoDB
69
func MongoDBProvider(ctx context.Context, instance v1beta1.GrowthbookInstance, username, password string) (storage.Disconnector, storage.Database, error) {
11✔
70
        opts := options.Client().ApplyURI(instance.Spec.MongoDB.URI)
11✔
71
        if username != "" || password != "" {
11✔
72
                opts.SetAuth(options.Credential{
×
73
                        Username: username,
×
74
                        Password: password,
×
75
                })
×
76
        }
×
77

78
        opts.SetAppName("k8sgrowthbook-controller")
11✔
79
        mongoClient, err := mongo.NewClient(opts)
11✔
80
        if err != nil {
22✔
81
                return nil, nil, err
11✔
82
        }
11✔
83

84
        u, err := url.Parse(instance.Spec.MongoDB.URI)
×
85
        if err != nil {
×
86
                return nil, nil, err
×
87
        }
×
88

89
        dbName := strings.TrimLeft(u.Path, "/")
×
90
        db := mongoClient.Database(dbName)
×
91

×
92
        if err := mongoClient.Connect(ctx); err != nil {
×
93
                return nil, nil, fmt.Errorf("failed connecting to mongodb: %w", err)
×
94
        }
×
95

96
        return mongoClient, mongodb.New(db), nil
×
97
}
98

99
// GrowthbookInstance reconciles a GrowthbookInstance object
100
type GrowthbookInstanceReconciler struct {
101
        client.Client
102
        Log              logr.Logger
103
        Scheme           *runtime.Scheme
104
        Recorder         record.EventRecorder
105
        DatabaseProvider func(ctx context.Context, instance v1beta1.GrowthbookInstance, username, password string) (storage.Disconnector, storage.Database, error)
106
}
107

108
type GrowthbookInstanceReconcilerOptions struct {
109
        MaxConcurrentReconciles int
110
}
111

112
// SetupWithManager adding controllers
113
func (r *GrowthbookInstanceReconciler) SetupWithManager(mgr ctrl.Manager, opts GrowthbookInstanceReconcilerOptions) error {
1✔
114
        // Index the GrowthbookInstance by the Secret references they point at
1✔
115
        if err := mgr.GetFieldIndexer().IndexField(context.TODO(), &v1beta1.GrowthbookInstance{}, secretIndexKey,
1✔
116
                func(o client.Object) []string {
196✔
117
                        // The referenced admin secret gets indexed
195✔
118
                        instance := o.(*v1beta1.GrowthbookInstance)
195✔
119
                        keys := []string{}
195✔
120

195✔
121
                        if instance.Spec.MongoDB.Secret != nil {
195✔
122
                                keys = []string{
×
123
                                        fmt.Sprintf("%s/%s", instance.GetNamespace(), instance.Spec.MongoDB.Secret.Name),
×
124
                                }
×
125
                        }
×
126

127
                        var users v1beta1.GrowthbookUserList
195✔
128
                        selector, err := metav1.LabelSelectorAsSelector(instance.Spec.ResourceSelector)
195✔
129
                        if err != nil {
195✔
130
                                return keys
×
131
                        }
×
132

133
                        err = r.Client.List(context.TODO(), &users, client.InNamespace(instance.Namespace), client.MatchingLabelsSelector{Selector: selector})
195✔
134
                        if err != nil {
195✔
135
                                return keys
×
136
                        }
×
137

138
                        for _, user := range users.Items {
279✔
139
                                if user.Spec.Secret == nil {
84✔
140
                                        continue
×
141
                                }
142

143
                                keys = append(keys, fmt.Sprintf("%s/%s", instance.GetNamespace(), user.Spec.Secret.Name))
84✔
144
                        }
145

146
                        var clients v1beta1.GrowthbookClientList
195✔
147
                        selector, err = metav1.LabelSelectorAsSelector(instance.Spec.ResourceSelector)
195✔
148
                        if err != nil {
195✔
149
                                return keys
×
150
                        }
×
151

152
                        err = r.Client.List(context.TODO(), &clients, client.InNamespace(instance.Namespace), client.MatchingLabelsSelector{Selector: selector})
195✔
153
                        if err != nil {
195✔
154
                                return keys
×
155
                        }
×
156

157
                        for _, client := range clients.Items {
323✔
158
                                if client.Spec.TokenSecret == nil {
128✔
159
                                        continue
×
160
                                }
161

162
                                keys = append(keys, fmt.Sprintf("%s/%s", instance.GetNamespace(), client.Spec.TokenSecret.Name))
128✔
163
                        }
164

165
                        return keys
195✔
166
                },
167
        ); err != nil {
×
168
                return err
×
169
        }
×
170

171
        return ctrl.NewControllerManagedBy(mgr).
1✔
172
                For(&v1beta1.GrowthbookInstance{}, builder.WithPredicates(
1✔
173
                        predicate.GenerationChangedPredicate{},
1✔
174
                )).
1✔
175
                Watches(
1✔
176
                        &corev1.Secret{},
1✔
177
                        handler.EnqueueRequestsFromMapFunc(r.requestsForChangeByField(secretIndexKey)),
1✔
178
                ).
1✔
179
                Watches(
1✔
180
                        &v1beta1.GrowthbookUser{},
1✔
181
                        handler.EnqueueRequestsFromMapFunc(r.requestsForChangeBySelector),
1✔
182
                ).
1✔
183
                Watches(
1✔
184
                        &v1beta1.GrowthbookOrganization{},
1✔
185
                        handler.EnqueueRequestsFromMapFunc(r.requestsForChangeBySelector),
1✔
186
                ).
1✔
187
                Watches(
1✔
188
                        &v1beta1.GrowthbookClient{},
1✔
189
                        handler.EnqueueRequestsFromMapFunc(r.requestsForChangeBySelector),
1✔
190
                ).
1✔
191
                Watches(
1✔
192
                        &v1beta1.GrowthbookFeature{},
1✔
193
                        handler.EnqueueRequestsFromMapFunc(r.requestsForChangeBySelector),
1✔
194
                ).
1✔
195
                WithOptions(controller.Options{MaxConcurrentReconciles: opts.MaxConcurrentReconciles}).
1✔
196
                Complete(r)
1✔
197
}
198

199
func (r *GrowthbookInstanceReconciler) requestsForChangeByField(field string) handler.MapFunc {
1✔
200
        return func(ctx context.Context, o client.Object) []reconcile.Request {
3✔
201
                var list v1beta1.GrowthbookInstanceList
2✔
202
                if err := r.List(ctx, &list, client.MatchingFields{
2✔
203
                        field: objectKey(o).String(),
2✔
204
                }); err != nil {
2✔
205
                        return nil
×
206
                }
×
207

208
                var reqs []reconcile.Request
2✔
209
                for _, instance := range list.Items {
4✔
210
                        r.Log.Info("change of referenced resource detected", "namespace", instance.GetNamespace(), "instance-name", instance.GetName(), "resource-kind", o.GetObjectKind().GroupVersionKind().Kind, "resource-name", o.GetName())
2✔
211
                        reqs = append(reqs, reconcile.Request{NamespacedName: objectKey(&instance)})
2✔
212
                }
2✔
213

214
                return reqs
2✔
215
        }
216
}
217

218
func (r *GrowthbookInstanceReconciler) requestsForChangeBySelector(ctx context.Context, o client.Object) []reconcile.Request {
54✔
219
        var list v1beta1.GrowthbookInstanceList
54✔
220
        if err := r.List(ctx, &list, client.InNamespace(o.GetNamespace())); err != nil {
54✔
221
                return nil
×
222
        }
×
223

224
        var reqs []reconcile.Request
54✔
225
        for _, instance := range list.Items {
281✔
226
                if matches(o.GetLabels(), instance.Spec.ResourceSelector) {
454✔
227
                        r.Log.Info("change of referenced resource detected", "namespace", o.GetNamespace(), "name", o.GetName(), "kind", o.GetObjectKind().GroupVersionKind().Kind, "instance-name", instance.GetName())
227✔
228
                        reqs = append(reqs, reconcile.Request{NamespacedName: objectKey(&instance)})
227✔
229
                }
227✔
230
        }
231

232
        return reqs
54✔
233
}
234

235
// Reconcile GrowthbookInstances
236
func (r *GrowthbookInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
110✔
237
        logger := r.Log.WithValues("Namespace", req.Namespace, "Name", req.NamespacedName, "req", req)
110✔
238
        logger.Info("reconciling GrowthbookInstance")
110✔
239

110✔
240
        // Fetch the GrowthbookInstance instance
110✔
241
        instance := v1beta1.GrowthbookInstance{}
110✔
242

110✔
243
        err := r.Client.Get(ctx, req.NamespacedName, &instance)
110✔
244
        if err != nil {
112✔
245
                if apierrors.IsNotFound(err) {
4✔
246
                        // Request object not found, could have been deleted after reconcile request.
2✔
247
                        // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers.
2✔
248
                        // Return and don't requeue
2✔
249
                        return reconcile.Result{}, nil
2✔
250
                }
2✔
251
                // Error reading the object - requeue the request.
252
                return reconcile.Result{}, err
×
253
        }
254

255
        if instance.Spec.Suspend {
117✔
256
                return ctrl.Result{}, nil
9✔
257
        }
9✔
258

259
        // examine DeletionTimestamp to determine if object is under deletion
260
        if err := r.addFinalizer(ctx, v1beta1.Finalizer, metav1.PartialObjectMetadata{TypeMeta: instance.TypeMeta, ObjectMeta: instance.ObjectMeta}); err != nil {
105✔
261
                return ctrl.Result{}, err
6✔
262
        }
6✔
263

264
        start := time.Now()
93✔
265

93✔
266
        reconcileContext := ctx
93✔
267
        if instance.Spec.Timeout != nil {
186✔
268
                c, cancel := context.WithTimeout(ctx, instance.Spec.Timeout.Duration)
93✔
269
                defer cancel()
93✔
270
                reconcileContext = c
93✔
271
        }
93✔
272

273
        instance, err = r.reconcile(reconcileContext, instance, logger)
93✔
274
        res := ctrl.Result{}
93✔
275

93✔
276
        done := time.Now()
93✔
277

93✔
278
        instance.Status.LastReconcileDuration = metav1.Duration{
93✔
279
                Duration: done.Sub(start),
93✔
280
        }
93✔
281

93✔
282
        instance.Status.ObservedGeneration = instance.GetGeneration()
93✔
283

93✔
284
        if err != nil {
141✔
285
                r.Recorder.Event(&instance, "Normal", "error", err.Error())
48✔
286
                res = ctrl.Result{Requeue: true}
48✔
287
                instance = v1beta1.GrowthbookInstanceNotReady(instance, v1beta1.FailedReason, err.Error())
48✔
288
        } else {
93✔
289
                if !instance.DeletionTimestamp.IsZero() {
47✔
290
                        if err := r.removeFinalizer(ctx, v1beta1.Finalizer, metav1.PartialObjectMetadata{TypeMeta: instance.TypeMeta, ObjectMeta: instance.ObjectMeta}); err != nil {
2✔
291
                                return res, err
×
292
                        } else {
2✔
293
                                return ctrl.Result{}, nil
2✔
294
                        }
2✔
295
                }
296

297
                if instance.Spec.Interval != nil {
43✔
298
                        res = ctrl.Result{
×
299
                                RequeueAfter: instance.Spec.Interval.Duration,
×
300
                        }
×
301
                }
×
302

303
                msg := "instance successfully reconciled"
43✔
304
                r.Recorder.Event(&instance, "Normal", "info", msg)
43✔
305
                instance = v1beta1.GrowthbookInstanceReady(instance, v1beta1.SynchronizedReason, msg)
43✔
306
        }
307

308
        // Update status after reconciliation.
309
        if err := r.patchStatus(ctx, &instance); err != nil {
101✔
310
                logger.Error(err, "unable to update status after reconciliation")
10✔
311
                return ctrl.Result{Requeue: true}, err
10✔
312
        }
10✔
313

314
        return res, err
81✔
315
}
316

317
func (r *GrowthbookInstanceReconciler) reconcile(ctx context.Context, instance v1beta1.GrowthbookInstance, logger logr.Logger) (v1beta1.GrowthbookInstance, error) {
93✔
318
        //TODO there is a test race condition with this one, leaving for now
93✔
319
        /*msg := "reconcile instance progressing"
93✔
320
        r.Recorder.Event(&instance, "Normal", "info", msg)
93✔
321
        instance = v1beta1.GrowthbookInstanceNotReady(instance, v1beta1.ProgressingReason, msg)
93✔
322
        if err := r.patchStatus(ctx, &instance); err != nil {
93✔
323
                return instance, err
93✔
324
        }*/
93✔
325

93✔
326
        var err error
93✔
327
        var usr, pw string
93✔
328
        if instance.Spec.MongoDB.Secret != nil {
93✔
329
                usr, pw, err = r.getUsernamePassword(ctx, instance, instance.Spec.MongoDB.Secret)
×
330
                if err != nil {
×
331
                        return instance, err
×
332
                }
×
333
        }
334

335
        disconnector, db, err := r.DatabaseProvider(ctx, instance, usr, pw)
93✔
336
        if err != nil {
115✔
337
                return instance, err
22✔
338
        }
22✔
339

340
        defer func() {
142✔
341
                ctx, cancel := context.WithTimeout(context.TODO(), time.Second*10)
71✔
342
                defer cancel()
71✔
343
                if err := disconnector.Disconnect(ctx); err != nil {
71✔
344
                        logger.Error(err, "failed disconnet mongodb")
×
345
                }
×
346
        }()
347

348
        instance.Status.SubResourceCatalog = []v1beta1.ResourceReference{}
71✔
349

71✔
350
        instance, err = r.reconcileUsers(ctx, instance, db, logger)
71✔
351
        if err != nil {
91✔
352
                return instance, fmt.Errorf("failed reconciling users: %w", err)
20✔
353
        }
20✔
354

355
        instance, orgs, err := r.reconcileOrganizations(ctx, instance, db, logger)
51✔
356
        if err != nil {
52✔
357
                return instance, fmt.Errorf("failed reconciling organizations: %w", err)
1✔
358
        }
1✔
359

360
        for _, org := range orgs {
92✔
361
                instance, err = r.reconcileFeatures(ctx, instance, org, db, logger)
42✔
362
                if err != nil {
42✔
363
                        return instance, fmt.Errorf("failed reconciling features: %w", err)
×
364
                }
×
365

366
                instance, err = r.reconcileClients(ctx, instance, org, db, logger)
42✔
367
                if err != nil {
47✔
368
                        return instance, fmt.Errorf("failed reconciling clients: %w", err)
5✔
369
                }
5✔
370
        }
371

372
        return instance, err
45✔
373
}
374

375
func (r *GrowthbookInstanceReconciler) reconcileOrganizations(ctx context.Context, instance v1beta1.GrowthbookInstance, db storage.Database, logger logr.Logger) (v1beta1.GrowthbookInstance, []v1beta1.GrowthbookOrganization, error) {
51✔
376
        var orgs v1beta1.GrowthbookOrganizationList
51✔
377
        finalizerName := fmt.Sprintf("%s/%s.%s", v1beta1.Finalizer, instance.Name, instance.Namespace)
51✔
378

51✔
379
        selector, err := metav1.LabelSelectorAsSelector(instance.Spec.ResourceSelector)
51✔
380
        if err != nil {
51✔
381
                return instance, nil, err
×
382
        }
×
383

384
        err = r.Client.List(ctx, &orgs, client.InNamespace(instance.Namespace), client.MatchingLabelsSelector{Selector: selector})
51✔
385
        if err != nil {
51✔
386
                return instance, nil, err
×
387
        }
×
388

389
        if instance.DeletionTimestamp.IsZero() {
100✔
390
                for _, org := range orgs.Items {
92✔
391
                        if err := r.addFinalizer(ctx, finalizerName, metav1.PartialObjectMetadata{TypeMeta: org.TypeMeta, ObjectMeta: org.ObjectMeta}); err != nil {
44✔
392
                                return instance, nil, err
1✔
393
                        }
1✔
394

395
                        if org.DeletionTimestamp.IsZero() {
82✔
396
                                instance = updateResourceCatalog(instance, &org)
40✔
397
                        }
40✔
398
                }
399
        }
400

401
        for _, org := range orgs.Items {
94✔
402
                o := growthbook.Organization{}
44✔
403
                o.FromV1beta1(org)
44✔
404

44✔
405
                for _, binding := range org.Spec.Users {
44✔
406
                        var users v1beta1.GrowthbookUserList
×
407
                        selector, err := metav1.LabelSelectorAsSelector(binding.Selector)
×
408
                        if err != nil {
×
409
                                return instance, nil, err
×
410
                        }
×
411

412
                        err = r.Client.List(ctx, &users, client.InNamespace(instance.Namespace), client.MatchingLabelsSelector{Selector: selector})
×
413
                        if err != nil {
×
414
                                return instance, nil, err
×
415
                        }
×
416

417
                        for _, user := range users.Items {
×
418
                                o.Members = append(o.Members, growthbook.OrganizationMember{
×
419
                                        ID:   user.GetID(),
×
420
                                        Role: binding.Role,
×
421
                                })
×
422
                        }
×
423
                }
424

425
                if org.DeletionTimestamp.IsZero() && instance.DeletionTimestamp.IsZero() {
84✔
426
                        if err := growthbook.UpdateOrganization(ctx, o, db); err != nil {
40✔
427
                                return instance, nil, err
×
428
                        }
×
429
                } else {
4✔
430
                        if instance.Spec.Prune {
6✔
431
                                if err := growthbook.DeleteOrganization(ctx, o, db); err != nil {
2✔
432
                                        return instance, nil, err
×
433
                                }
×
434
                        }
435

436
                        if err := r.removeFinalizer(ctx, finalizerName, metav1.PartialObjectMetadata{TypeMeta: org.TypeMeta, ObjectMeta: org.ObjectMeta}); err != nil {
4✔
437
                                return instance, nil, err
×
438
                        }
×
439
                }
440
        }
441

442
        return instance, orgs.Items, nil
50✔
443
}
444

445
func (r *GrowthbookInstanceReconciler) reconcileFeatures(ctx context.Context, instance v1beta1.GrowthbookInstance, org v1beta1.GrowthbookOrganization, db storage.Database, logger logr.Logger) (v1beta1.GrowthbookInstance, error) {
42✔
446
        var features v1beta1.GrowthbookFeatureList
42✔
447
        selector, err := metav1.LabelSelectorAsSelector(org.Spec.ResourceSelector)
42✔
448
        if err != nil {
42✔
449
                return instance, err
×
450
        }
×
451

452
        finalizerName := fmt.Sprintf("%s/%s.%s", v1beta1.Finalizer, instance.Name, instance.Namespace)
42✔
453
        instanceSelector, err := metav1.LabelSelectorAsSelector(instance.Spec.ResourceSelector)
42✔
454
        if err != nil {
42✔
455
                return instance, err
×
456
        }
×
457

458
        req, _ := instanceSelector.Requirements()
42✔
459
        selector.Add(req...)
42✔
460

42✔
461
        err = r.Client.List(ctx, &features, client.InNamespace(instance.Namespace), client.MatchingLabelsSelector{Selector: selector})
42✔
462
        if err != nil {
42✔
463
                return instance, err
×
464
        }
×
465

466
        if instance.DeletionTimestamp.IsZero() {
82✔
467
                for _, feature := range features.Items {
77✔
468
                        if err := r.addFinalizer(ctx, finalizerName, metav1.PartialObjectMetadata{TypeMeta: feature.TypeMeta, ObjectMeta: feature.ObjectMeta}); err != nil {
37✔
469
                                return instance, err
×
470
                        }
×
471

472
                        if feature.DeletionTimestamp.IsZero() {
74✔
473
                                instance = updateResourceCatalog(instance, &feature)
37✔
474
                        }
37✔
475
                }
476
        }
477

478
        for _, feature := range features.Items {
79✔
479
                f := growthbook.Feature{
37✔
480
                        Owner:        owner,
37✔
481
                        Organization: org.GetID(),
37✔
482
                }
37✔
483

37✔
484
                f.FromV1beta1(feature)
37✔
485

37✔
486
                if feature.DeletionTimestamp.IsZero() && instance.DeletionTimestamp.IsZero() {
74✔
487
                        if err := growthbook.UpdateFeature(ctx, f, db); err != nil {
37✔
488
                                return instance, err
×
489
                        }
×
490
                } else {
×
491
                        if instance.Spec.Prune {
×
492
                                if err := growthbook.DeleteFeature(ctx, f, db); err != nil {
×
493
                                        return instance, err
×
494
                                }
×
495
                        }
496

497
                        if err := r.removeFinalizer(ctx, finalizerName, metav1.PartialObjectMetadata{TypeMeta: feature.TypeMeta, ObjectMeta: feature.ObjectMeta}); err != nil {
×
498
                                return instance, err
×
499
                        }
×
500
                }
501
        }
502

503
        return instance, nil
42✔
504
}
505

506
func (r *GrowthbookInstanceReconciler) addFinalizer(ctx context.Context, finalizerName string, obj metav1.PartialObjectMetadata) error {
247✔
507
        if !obj.GetDeletionTimestamp().IsZero() {
251✔
508
                return nil
4✔
509
        }
4✔
510

511
        controllerutil.AddFinalizer(&obj, finalizerName)
243✔
512
        if err := r.patch(ctx, &obj); err != nil {
250✔
513
                return err
7✔
514
        }
7✔
515

516
        return nil
236✔
517
}
518

519
func (r *GrowthbookInstanceReconciler) removeFinalizer(ctx context.Context, finalizerName string, obj metav1.PartialObjectMetadata) error {
6✔
520
        controllerutil.RemoveFinalizer(&obj, finalizerName)
6✔
521
        if err := r.patch(ctx, &obj); err != nil {
6✔
522
                return err
×
523
        }
×
524

525
        return nil
6✔
526
}
527

528
func (r *GrowthbookInstanceReconciler) reconcileUsers(ctx context.Context, instance v1beta1.GrowthbookInstance, db storage.Database, logger logr.Logger) (v1beta1.GrowthbookInstance, error) {
71✔
529
        var users v1beta1.GrowthbookUserList
71✔
530
        finalizerName := fmt.Sprintf("%s/%s.%s", v1beta1.Finalizer, instance.Name, instance.Namespace)
71✔
531

71✔
532
        selector, err := metav1.LabelSelectorAsSelector(instance.Spec.ResourceSelector)
71✔
533
        if err != nil {
71✔
534
                return instance, err
×
535
        }
×
536

537
        err = r.Client.List(ctx, &users, client.InNamespace(instance.Namespace), client.MatchingLabelsSelector{Selector: selector})
71✔
538
        if err != nil {
71✔
539
                return instance, err
×
540
        }
×
541

542
        if instance.DeletionTimestamp.IsZero() {
140✔
543
                for _, user := range users.Items {
111✔
544
                        if err := r.addFinalizer(ctx, finalizerName, metav1.PartialObjectMetadata{TypeMeta: user.TypeMeta, ObjectMeta: user.ObjectMeta}); err != nil {
42✔
545
                                return instance, err
×
546
                        }
×
547

548
                        if user.DeletionTimestamp.IsZero() {
84✔
549
                                instance = updateResourceCatalog(instance, &user)
42✔
550
                        }
42✔
551
                }
552
        }
553

554
        for _, user := range users.Items {
110✔
555
                u := growthbook.User{}
39✔
556
                u.FromV1beta1(user)
39✔
557

39✔
558
                if user.DeletionTimestamp.IsZero() && instance.DeletionTimestamp.IsZero() {
78✔
559
                        username, password, err := r.getOptionalUsernamePassword(ctx, instance, user.Spec.Secret)
39✔
560
                        if err != nil {
59✔
561
                                return instance, err
20✔
562
                        }
20✔
563

564
                        if username != "" {
19✔
565
                                u.Email = username
×
566
                        }
×
567

568
                        if err := u.SetPassword(ctx, db, password); err != nil {
19✔
569
                                return instance, err
×
570
                        }
×
571

572
                        if err := growthbook.UpdateUser(ctx, u, db); err != nil {
19✔
573
                                return instance, err
×
574
                        }
×
575
                } else {
×
576
                        if instance.Spec.Prune {
×
577
                                if err := growthbook.DeleteUser(ctx, u, db); err != nil {
×
578
                                        return instance, err
×
579
                                }
×
580
                        }
581

582
                        if err := r.removeFinalizer(ctx, finalizerName, metav1.PartialObjectMetadata{TypeMeta: user.TypeMeta, ObjectMeta: user.ObjectMeta}); err != nil {
×
583
                                return instance, err
×
584
                        }
×
585
                }
586
        }
587

588
        return instance, nil
51✔
589
}
590

591
func (r *GrowthbookInstanceReconciler) reconcileClients(ctx context.Context, instance v1beta1.GrowthbookInstance, org v1beta1.GrowthbookOrganization, db storage.Database, logger logr.Logger) (v1beta1.GrowthbookInstance, error) {
42✔
592
        var clients v1beta1.GrowthbookClientList
42✔
593
        finalizerName := fmt.Sprintf("%s/%s.%s", v1beta1.Finalizer, instance.Name, instance.Namespace)
42✔
594

42✔
595
        selector, err := metav1.LabelSelectorAsSelector(org.Spec.ResourceSelector)
42✔
596
        if err != nil {
42✔
597
                return instance, err
×
598
        }
×
599

600
        instanceSelector, err := metav1.LabelSelectorAsSelector(instance.Spec.ResourceSelector)
42✔
601
        if err != nil {
42✔
602
                return instance, err
×
603
        }
×
604

605
        req, _ := instanceSelector.Requirements()
42✔
606
        selector.Add(req...)
42✔
607

42✔
608
        err = r.Client.List(ctx, &clients, client.InNamespace(instance.Namespace), client.MatchingLabelsSelector{Selector: selector})
42✔
609
        if err != nil {
42✔
610
                return instance, err
×
611
        }
×
612

613
        if instance.DeletionTimestamp.IsZero() {
82✔
614
                for _, client := range clients.Items {
66✔
615
                        if err := r.addFinalizer(ctx, finalizerName, metav1.PartialObjectMetadata{TypeMeta: client.TypeMeta, ObjectMeta: client.ObjectMeta}); err != nil {
26✔
616
                                return instance, err
×
617
                        }
×
618

619
                        if client.DeletionTimestamp.IsZero() {
52✔
620
                                instance = updateResourceCatalog(instance, &client)
26✔
621
                        }
26✔
622
                }
623
        }
624

625
        for _, client := range clients.Items {
67✔
626
                s := growthbook.SDKConnection{
25✔
627
                        Organization: org.GetID(),
25✔
628
                }
25✔
629

25✔
630
                s.FromV1beta1(client)
25✔
631

25✔
632
                if client.DeletionTimestamp.IsZero() && instance.DeletionTimestamp.IsZero() {
50✔
633
                        token, err := r.getClientToken(ctx, client)
25✔
634
                        if err != nil {
30✔
635
                                return instance, err
5✔
636
                        }
5✔
637

638
                        if token[:4] != "sdk-" {
40✔
639
                                token = fmt.Sprintf("sdk-%s", token)
20✔
640
                        }
20✔
641

642
                        s.Key = token
20✔
643

20✔
644
                        if err := growthbook.UpdateSDKConnection(ctx, s, db); err != nil {
20✔
645
                                return instance, err
×
646
                        }
×
647
                } else {
×
648
                        if instance.Spec.Prune {
×
649
                                if err := growthbook.DeleteSDKConnection(ctx, s, db); err != nil {
×
650
                                        return instance, err
×
651
                                }
×
652
                        }
653

654
                        if err := r.removeFinalizer(ctx, finalizerName, metav1.PartialObjectMetadata{TypeMeta: client.TypeMeta, ObjectMeta: client.ObjectMeta}); err != nil {
×
655
                                return instance, err
×
656
                        }
×
657
                }
658

659
        }
660

661
        return instance, nil
37✔
662
}
663

664
func (r *GrowthbookInstanceReconciler) getSecret(ctx context.Context, ref types.NamespacedName) (*corev1.Secret, error) {
64✔
665
        secret := &corev1.Secret{}
64✔
666
        err := r.Client.Get(ctx, ref, secret)
64✔
667

64✔
668
        if err != nil {
89✔
669
                return nil, fmt.Errorf("referencing secret was not found: %w", err)
25✔
670
        }
25✔
671

672
        return secret, nil
39✔
673
}
674

675
func (r *GrowthbookInstanceReconciler) getClientToken(ctx context.Context, client v1beta1.GrowthbookClient) (string, error) {
25✔
676
        if client.Spec.TokenSecret == nil {
25✔
677
                return "", errors.New("no secret reference provided")
×
678
        }
×
679

680
        secret, err := r.getSecret(ctx, types.NamespacedName{
25✔
681
                Namespace: client.Namespace,
25✔
682
                Name:      client.Spec.TokenSecret.Name,
25✔
683
        })
25✔
684

25✔
685
        if err != nil {
30✔
686
                return "", err
5✔
687
        }
5✔
688

689
        tokenFieldName := "token"
20✔
690
        if client.Spec.TokenSecret.TokenField != "" {
40✔
691
                tokenFieldName = client.Spec.TokenSecret.TokenField
20✔
692
        }
20✔
693

694
        if val, ok := secret.Data[tokenFieldName]; !ok {
20✔
695
                return "", errors.New("defined token field not found in secret")
×
696
        } else {
20✔
697
                return string(val), nil
20✔
698
        }
20✔
699
}
700

701
func updateResourceCatalog(instance v1beta1.GrowthbookInstance, resource client.Object) v1beta1.GrowthbookInstance {
145✔
702
        resRef := v1beta1.ResourceReference{
145✔
703
                Kind:       resource.GetObjectKind().GroupVersionKind().Kind,
145✔
704
                Name:       resource.GetName(),
145✔
705
                APIVersion: fmt.Sprintf("%s/%s", resource.GetObjectKind().GroupVersionKind().Group, resource.GetObjectKind().GroupVersionKind().Version),
145✔
706
        }
145✔
707

145✔
708
        if !slices.Contains(instance.Status.SubResourceCatalog, resRef) {
282✔
709
                instance.Status.SubResourceCatalog = append(instance.Status.SubResourceCatalog, resRef)
137✔
710
        }
137✔
711

712
        return instance
145✔
713
}
714

715
func (r *GrowthbookInstanceReconciler) getUsernamePassword(ctx context.Context, instance v1beta1.GrowthbookInstance, secretReference *v1beta1.SecretReference) (string, string, error) {
×
716
        if secretReference == nil {
×
717
                return "", "", errors.New("no secret reference provided")
×
718
        }
×
719

720
        secret, err := r.getSecret(ctx, types.NamespacedName{
×
721
                Namespace: instance.Namespace,
×
722
                Name:      secretReference.Name,
×
723
        })
×
724

×
725
        if err != nil {
×
726
                return "", "", err
×
727
        }
×
728

729
        var (
×
730
                user string
×
731
                pw   string
×
732
        )
×
733

×
734
        if val, ok := secret.Data[secretReference.UserField]; !ok {
×
735
                return "", "", errors.New("defined username field not found in secret")
×
736
        } else {
×
737
                user = string(val)
×
738
        }
×
739

740
        if val, ok := secret.Data[secretReference.PasswordField]; !ok {
×
741
                return "", "", errors.New("defined password field not found in secret")
×
742
        } else {
×
743
                pw = string(val)
×
744
        }
×
745

746
        return user, pw, nil
×
747
}
748

749
func (r *GrowthbookInstanceReconciler) getOptionalUsernamePassword(ctx context.Context, instance v1beta1.GrowthbookInstance, secretReference *v1beta1.SecretReference) (string, string, error) {
39✔
750
        if secretReference == nil {
39✔
751
                return "", "", errors.New("no secret reference provided")
×
752
        }
×
753

754
        secret, err := r.getSecret(ctx, types.NamespacedName{
39✔
755
                Namespace: instance.Namespace,
39✔
756
                Name:      secretReference.Name,
39✔
757
        })
39✔
758

39✔
759
        if err != nil {
59✔
760
                return "", "", err
20✔
761
        }
20✔
762

763
        var (
19✔
764
                user string
19✔
765
                pw   string
19✔
766
        )
19✔
767

19✔
768
        if val, ok := secret.Data[secretReference.UserField]; ok {
19✔
769
                user = string(val)
×
770
        }
×
771

772
        if val, ok := secret.Data[secretReference.PasswordField]; !ok {
19✔
773
                return "", "", errors.New("defined password field not found in secret")
×
774
        } else {
19✔
775
                pw = string(val)
19✔
776
        }
19✔
777

778
        return user, pw, nil
19✔
779
}
780

781
func (r *GrowthbookInstanceReconciler) patch(ctx context.Context, obj *metav1.PartialObjectMetadata) error {
249✔
782
        key := client.ObjectKeyFromObject(obj)
249✔
783
        latest := &metav1.PartialObjectMetadata{
249✔
784
                TypeMeta: obj.TypeMeta,
249✔
785
        }
249✔
786

249✔
787
        if err := r.Client.Get(ctx, key, latest); err != nil {
250✔
788
                return err
1✔
789
        }
1✔
790

791
        return r.Client.Patch(ctx, obj, client.MergeFrom(latest))
248✔
792
}
793

794
func (r *GrowthbookInstanceReconciler) patchStatus(ctx context.Context, instance *v1beta1.GrowthbookInstance) error {
91✔
795
        key := client.ObjectKeyFromObject(instance)
91✔
796
        latest := &v1beta1.GrowthbookInstance{}
91✔
797
        if err := r.Client.Get(ctx, key, latest); err != nil {
91✔
798
                return err
×
799
        }
×
800

801
        return r.Client.Status().Patch(ctx, instance, client.MergeFrom(latest))
91✔
802
}
803

804
// objectKey returns client.ObjectKey for the object.
805
func objectKey(object metav1.Object) client.ObjectKey {
231✔
806
        return client.ObjectKey{
231✔
807
                Namespace: object.GetNamespace(),
231✔
808
                Name:      object.GetName(),
231✔
809
        }
231✔
810
}
231✔
811

812
func matches(labels map[string]string, selector *metav1.LabelSelector) bool {
227✔
813
        if selector == nil {
293✔
814
                return true
66✔
815
        }
66✔
816

817
        for kS, vS := range selector.MatchLabels {
276✔
818
                var match bool
115✔
819
                for kL, vL := range selector.MatchLabels {
230✔
820
                        if kS == kL && vS == vL {
230✔
821
                                match = true
115✔
822
                        }
115✔
823
                }
824

825
                if !match {
115✔
826
                        return false
×
827
                }
×
828
        }
829

830
        return true
161✔
831
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc