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

chideat / valkey-operator / 15432139607

04 Jun 2025 02:28AM UTC coverage: 19.328% (+9.0%) from 10.284%
15432139607

Pull #18

github

chideat
fix: fix type validation
Pull Request #18: chore: added more unit tests

191 of 552 new or added lines in 46 files covered. (34.6%)

10 existing lines in 5 files now uncovered.

3869 of 20018 relevant lines covered (19.33%)

0.23 hits per line

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

18.99
/internal/controller/user_controller.go
1
/*
2
Copyright 2024 chideat.
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 controller
18

19
import (
20
        "context"
21
        "fmt"
22
        "reflect"
23
        "strings"
24
        "time"
25

26
        "github.com/chideat/valkey-operator/api/core"
27
        "github.com/chideat/valkey-operator/api/v1alpha1"
28
        "github.com/chideat/valkey-operator/internal/builder"
29
        "github.com/chideat/valkey-operator/internal/config"
30
        "github.com/chideat/valkey-operator/internal/controller/user"
31
        "github.com/chideat/valkey-operator/internal/util"
32
        security "github.com/chideat/valkey-operator/pkg/security/password"
33

34
        corev1 "k8s.io/api/core/v1"
35
        "k8s.io/apimachinery/pkg/api/errors"
36
        "k8s.io/apimachinery/pkg/runtime"
37
        "k8s.io/apimachinery/pkg/types"
38
        "k8s.io/client-go/tools/record"
39
        "k8s.io/client-go/util/retry"
40
        ctrl "sigs.k8s.io/controller-runtime"
41
        "sigs.k8s.io/controller-runtime/pkg/client"
42
        "sigs.k8s.io/controller-runtime/pkg/controller"
43
        "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
44
        "sigs.k8s.io/controller-runtime/pkg/event"
45
        "sigs.k8s.io/controller-runtime/pkg/log"
46
        "sigs.k8s.io/controller-runtime/pkg/predicate"
47
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
48
)
49

50
const (
51
        UserFinalizer = "buf.red/user-finalizer"
52
)
53

54
// UserReconciler reconciles a User object
55
type UserReconciler struct {
56
        client.Client
57
        Scheme *runtime.Scheme
58

59
        EventRecorder record.EventRecorder
60
        Handler       *user.UserHandler
61
}
62

63
// +kubebuilder:rbac:groups=valkey.buf.red,resources=users,verbs=get;list;watch;create;update;patch;delete
64
// +kubebuilder:rbac:groups=valkey.buf.red,resources=users/status,verbs=get;update;patch
65
// +kubebuilder:rbac:groups=valkey.buf.red,resources=users/finalizers,verbs=update
66

67
// Reconcile is part of the main kubernetes reconciliation loop which aims to
68
// move the current state of the cluster closer to the desired state.
69
func (r *UserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
1✔
70
        logger := log.FromContext(ctx).WithName("User").WithValues("target", req.String())
1✔
71

1✔
72
        instance := v1alpha1.User{}
1✔
73
        err := r.Client.Get(ctx, req.NamespacedName, &instance)
1✔
74
        if err != nil {
1✔
75
                logger.Error(err, "get valkey user failed")
×
76
                if errors.IsNotFound(err) {
×
77
                        return reconcile.Result{}, nil
×
78
                }
×
79
                return reconcile.Result{}, err
×
80
        }
81

82
        isMarkedToBeDeleted := instance.GetDeletionTimestamp() != nil
1✔
83
        if isMarkedToBeDeleted {
1✔
NEW
84
                if err := r.Handler.Delete(ctx, instance, logger); err != nil {
×
NEW
85
                        if instance.Status.Message != err.Error() {
×
NEW
86
                                instance.Status.Phase = v1alpha1.UserFail
×
NEW
87
                                instance.Status.Message = fmt.Sprintf("clean user failed with error %s", err.Error())
×
NEW
88
                                if err := r.Client.Status().Update(ctx, &instance); err != nil {
×
NEW
89
                                        logger.Error(err, "update user status failed", "instance", req.NamespacedName)
×
NEW
90
                                        return ctrl.Result{}, err
×
NEW
91
                                }
×
92
                        }
NEW
93
                        return ctrl.Result{RequeueAfter: time.Second * 10}, err
×
NEW
94
                } else {
×
NEW
95
                        controllerutil.RemoveFinalizer(&instance, UserFinalizer)
×
NEW
96
                        if err := r.Update(ctx, &instance); err != nil {
×
NEW
97
                                return ctrl.Result{RequeueAfter: time.Second * 10}, nil
×
NEW
98
                        }
×
99
                }
100
                return ctrl.Result{}, nil
×
101
        }
102

103
        vkName := instance.Spec.InstanceName
1✔
104
        switch instance.Spec.Arch {
1✔
105
        case core.ValkeySentinel, core.ValkeyReplica:
×
106
                rf := &v1alpha1.Failover{}
×
107
                if err := r.Get(ctx, types.NamespacedName{Namespace: instance.Namespace, Name: vkName}, rf); err != nil {
×
108
                        if errors.IsNotFound(err) {
×
109
                                logger.Error(err, "instance not found", "name", vkName)
×
110
                                return ctrl.Result{Requeue: true}, r.Delete(ctx, &instance)
×
111
                        }
×
112
                        logger.Error(err, "get failover failed", "name", instance.Name)
×
113
                        return ctrl.Result{}, err
×
114
                }
115
        case core.ValkeyCluster:
1✔
116
                cluster := &v1alpha1.Cluster{}
1✔
117
                if err := r.Get(ctx, types.NamespacedName{Namespace: instance.Namespace, Name: vkName}, cluster); err != nil {
1✔
118
                        if errors.IsNotFound(err) {
×
119
                                logger.Error(err, "instance not found", "name", vkName)
×
120
                                return ctrl.Result{Requeue: true}, r.Delete(ctx, &instance)
×
121
                        }
×
122
                        logger.Error(err, "get cluster instance failed", "name", instance.Name)
×
123
                        return ctrl.Result{}, err
×
124
                }
125
        }
126

127
        // verify user password
128
        for _, name := range instance.Spec.PasswordSecrets {
1✔
129
                if name == "" {
×
130
                        continue
×
131
                }
132
                secret := &corev1.Secret{}
×
133
                if err := r.Get(ctx, types.NamespacedName{
×
134
                        Namespace: instance.Namespace,
×
135
                        Name:      name,
×
136
                }, secret); err != nil {
×
137
                        logger.Error(err, "get secret failed", "secret name", name)
×
138
                        instance.Status.Message = err.Error()
×
139
                        instance.Status.Phase = v1alpha1.UserFail
×
140
                        if e := r.Client.Status().Update(ctx, &instance); e != nil {
×
141
                                logger.Error(e, "update User status to Fail failed")
×
142
                        }
×
143
                        return ctrl.Result{}, err
×
144
                } else if err := security.PasswordValidate(string(secret.Data["password"]), 8, 32); err != nil {
×
145
                        if instance.Spec.AccountType != v1alpha1.SystemAccount {
×
146
                                instance.Status.Message = err.Error()
×
147
                                instance.Status.Phase = v1alpha1.UserFail
×
148
                                if e := r.Client.Status().Update(ctx, &instance); e != nil {
×
149
                                        logger.Error(e, "update User status to Fail failed")
×
150
                                }
×
151
                                return ctrl.Result{}, err
×
152
                        }
153
                }
154

155
                if secret.GetLabels() == nil {
×
156
                        secret.SetLabels(map[string]string{})
×
157
                }
×
158
                if secret.Labels[builder.InstanceNameLabelKey] != vkName ||
×
159
                        len(secret.GetOwnerReferences()) == 0 || secret.OwnerReferences[0].UID != instance.GetUID() {
×
160

×
161
                        secret.Labels[builder.ManagedByLabelKey] = config.AppName
×
162
                        secret.Labels[builder.InstanceNameLabelKey] = vkName
×
163
                        secret.OwnerReferences = util.BuildOwnerReferences(&instance)
×
164
                        if err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
×
NEW
165
                                return r.Update(ctx, secret)
×
166
                        }); err != nil {
×
167
                                logger.Error(err, "update secret owner failed", "secret", secret.Name)
×
168
                                instance.Status.Message = err.Error()
×
169
                                instance.Status.Phase = v1alpha1.UserFail
×
170
                                return ctrl.Result{RequeueAfter: time.Second * 5}, r.Client.Status().Update(ctx, &instance)
×
171
                        }
×
172
                }
173
        }
174

175
        if err := r.Handler.Do(ctx, instance, logger); err != nil {
2✔
176
                if strings.Contains(err.Error(), "instance is not ready") ||
1✔
177
                        strings.Contains(err.Error(), "node not ready") ||
1✔
178
                        strings.Contains(err.Error(), "user not operator") ||
1✔
179
                        strings.Contains(err.Error(), "ERR unknown command `ACL`") {
1✔
180
                        logger.V(3).Info("instance is not ready", "instance", vkName)
×
181
                        instance.Status.Message = err.Error()
×
182
                        instance.Status.Phase = v1alpha1.UserPending
×
183
                        if err := r.updateUserStatus(ctx, &instance); err != nil {
×
184
                                logger.Error(err, "update User status to Pending failed")
×
185
                        }
×
186
                        return ctrl.Result{RequeueAfter: time.Second * 15}, nil
×
187
                }
188

189
                instance.Status.Message = err.Error()
1✔
190
                instance.Status.Phase = v1alpha1.UserFail
1✔
191
                logger.Error(err, "user reconcile failed")
1✔
192
                if err := r.updateUserStatus(ctx, &instance); err != nil {
1✔
193
                        logger.Error(err, "update User status to Fail failed")
×
194
                }
×
195
                return reconcile.Result{RequeueAfter: time.Second * 10}, nil
1✔
196
        }
NEW
197
        instance.Status.Phase = v1alpha1.UserReady
×
198
        instance.Status.Message = ""
×
199
        logger.V(3).Info("user reconcile success")
×
200
        if err := r.updateUserStatus(ctx, &instance); err != nil {
×
201
                logger.Error(err, "update User status to Success failed")
×
202
                return reconcile.Result{RequeueAfter: time.Second * 10}, err
×
203
        }
×
204
        if !controllerutil.ContainsFinalizer(&instance, UserFinalizer) {
×
205
                controllerutil.AddFinalizer(&instance, UserFinalizer)
×
206
                if err := r.updateUser(ctx, &instance); err != nil {
×
207
                        logger.Error(err, "update finalizer user failed")
×
208
                        return ctrl.Result{}, err
×
209
                }
×
210
        }
211

212
        return ctrl.Result{}, nil
×
213
}
214

215
func (r *UserReconciler) updateUserStatus(ctx context.Context, inst *v1alpha1.User) error {
1✔
216
        return retry.RetryOnConflict(retry.DefaultRetry, func() error {
2✔
217
                var oldUser v1alpha1.User
1✔
218
                if err := r.Get(ctx, types.NamespacedName{Namespace: inst.Namespace, Name: inst.Name}, &oldUser); err != nil {
1✔
219
                        return err
×
220
                }
×
221
                inst.ResourceVersion = oldUser.ResourceVersion
1✔
222
                return r.Status().Update(ctx, inst)
1✔
223
        })
224
}
225

226
func (r *UserReconciler) updateUser(ctx context.Context, inst *v1alpha1.User) error {
×
227
        return retry.RetryOnConflict(retry.DefaultRetry, func() error {
×
228
                var oldUser v1alpha1.User
×
229
                if err := r.Get(ctx, types.NamespacedName{Namespace: inst.Namespace, Name: inst.Name}, &oldUser); err != nil {
×
230
                        return err
×
231
                }
×
232
                inst.ResourceVersion = oldUser.ResourceVersion
×
233
                return r.Update(ctx, inst)
×
234
        })
235
}
236

237
// SetupWithManager sets up the controller with the Manager.
238
func (r *UserReconciler) SetupWithManager(mgr ctrl.Manager) error {
×
239
        return ctrl.NewControllerManagedBy(mgr).
×
240
                For(&v1alpha1.User{}).
×
241
                Owns(&corev1.Secret{}).
×
242
                WithEventFilter(generationChangedFilter()).
×
243
                WithOptions(controller.Options{MaxConcurrentReconciles: 8}).
×
244
                Complete(r)
×
245
}
×
246

247
func generationChangedFilter() predicate.Predicate {
×
248
        return predicate.Funcs{
×
249
                UpdateFunc: func(e event.UpdateEvent) bool {
×
250
                        if e.ObjectOld == nil {
×
251
                                return false
×
252
                        }
×
253
                        if e.ObjectNew == nil {
×
254
                                return false
×
255
                        }
×
256
                        if reflect.TypeOf(e.ObjectNew).String() == reflect.TypeOf(&corev1.Secret{}).String() {
×
257
                                return true
×
258
                        }
×
259
                        // Ignore updates to CR status in which case metadata.Generation does not change
260
                        return e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration()
×
261
                },
262
        }
263
}
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

© 2025 Coveralls, Inc