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

SickHub / mailu-operator / 17348864456

30 Aug 2025 09:38PM UTC coverage: 38.019% (-0.09%) from 38.11%
17348864456

push

github

web-flow
fix: stop using ctrl.Result.Requeue (#135)

59 of 65 new or added lines in 3 files covered. (90.77%)

6 existing lines in 3 files now uncovered.

614 of 1615 relevant lines covered (38.02%)

0.42 hits per line

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

71.83
/internal/controller/alias_controller.go
1
package controller
2

3
import (
4
        "context"
5
        "encoding/json"
6
        "errors"
7
        "fmt"
8
        "io"
9
        "net/http"
10
        "reflect"
11
        "strconv"
12
        "time"
13

14
        "k8s.io/apimachinery/pkg/api/meta"
15
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16
        "k8s.io/apimachinery/pkg/runtime"
17
        ctrl "sigs.k8s.io/controller-runtime"
18
        "sigs.k8s.io/controller-runtime/pkg/client"
19
        "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
20
        "sigs.k8s.io/controller-runtime/pkg/log"
21
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
22

23
        operatorv1alpha1 "github.com/sickhub/mailu-operator/api/v1alpha1"
24
        "github.com/sickhub/mailu-operator/pkg/mailu"
25
)
26

27
const (
28
        AliasConditionTypeReady = "AliasReady"
29
)
30

31
// AliasReconciler reconciles a Alias object
32
type AliasReconciler struct {
33
        client.Client
34
        Scheme    *runtime.Scheme
35
        ApiURL    string
36
        ApiToken  string
37
        ApiClient *mailu.Client
38
}
39

40
//+kubebuilder:rbac:groups=operator.mailu.io,resources=aliases,verbs=get;list;watch;create;update;patch;delete
41
//+kubebuilder:rbac:groups=operator.mailu.io,resources=aliases/status,verbs=get;update;patch
42
//+kubebuilder:rbac:groups=operator.mailu.io,resources=aliases/finalizers,verbs=update
43

44
// Reconcile is part of the main kubernetes reconciliation loop which aims to
45
// move the current state of the cluster closer to the desired state.
46
//
47
// For more details, check Reconcile and its Result here:
48
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.3/pkg/reconcile
49
func (r *AliasReconciler) Reconcile(ctx context.Context, alias *operatorv1alpha1.Alias) (ctrl.Result, error) {
1✔
50
        logr := log.FromContext(ctx)
1✔
51

1✔
52
        aliasOriginal := alias.DeepCopy()
1✔
53

1✔
54
        // apply patches at the end, before returning
1✔
55
        defer func() {
2✔
56
                if err := r.Patch(ctx, alias.DeepCopy(), client.MergeFrom(aliasOriginal)); err != nil {
1✔
57
                        logr.Error(err, "failed to patch resource")
×
58
                }
×
59
                if err := r.Status().Patch(ctx, alias.DeepCopy(), client.MergeFrom(aliasOriginal)); err != nil {
2✔
60
                        logr.Error(err, "failed to patch resource status")
1✔
61
                }
1✔
62
        }()
63

64
        if alias.DeletionTimestamp == nil && !controllerutil.ContainsFinalizer(alias, FinalizerName) {
2✔
65
                controllerutil.AddFinalizer(alias, FinalizerName)
1✔
66
        }
1✔
67

68
        result, err := r.reconcile(ctx, alias)
1✔
69
        if err != nil {
1✔
70
                return result, err
×
71
        }
×
72

73
        if aliasOriginal.DeletionTimestamp != nil && result.RequeueAfter == 0 {
2✔
74
                controllerutil.RemoveFinalizer(alias, FinalizerName)
1✔
75
        }
1✔
76

77
        return result, nil
1✔
78
}
79

80
func (r *AliasReconciler) reconcile(ctx context.Context, alias *operatorv1alpha1.Alias) (ctrl.Result, error) {
1✔
81
        logr := log.FromContext(ctx)
1✔
82

1✔
83
        if r.ApiClient == nil {
2✔
84
                api, err := mailu.NewClient(r.ApiURL, mailu.WithRequestEditorFn(func(ctx context.Context, req *http.Request) error {
2✔
85
                        req.Header.Add("Authorization", "Bearer "+r.ApiToken)
1✔
86
                        return nil
1✔
87
                }))
1✔
88
                if err != nil {
1✔
89
                        return ctrl.Result{}, err
×
90
                }
×
91
                r.ApiClient = api
1✔
92
        }
93

94
        foundAlias, retry, err := r.getAlias(ctx, alias)
1✔
95
        if err != nil {
2✔
96
                if retry {
2✔
97
                        logr.Info(fmt.Errorf("failed to get alias, requeueing: %w", err).Error())
1✔
98
                        return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
1✔
99
                }
1✔
100
                // we explicitly set the error in the status only on a permanent (non-retryable) error
101
                meta.SetStatusCondition(&alias.Status.Conditions, getAliasReadyCondition(metav1.ConditionFalse, "Error", err.Error()))
1✔
102
                logr.Error(err, "failed to get alias")
1✔
103
                return ctrl.Result{}, nil
1✔
104
        }
105

106
        if alias.DeletionTimestamp != nil {
2✔
107
                if foundAlias == nil {
2✔
108
                        // no need to delete it, if it does not exist
1✔
109
                        return ctrl.Result{}, nil
1✔
110
                }
1✔
111
                return r.delete(ctx, alias)
1✔
112
        }
113

114
        if foundAlias == nil {
2✔
115
                return r.create(ctx, alias)
1✔
116
        }
1✔
117

118
        return r.update(ctx, alias, foundAlias)
1✔
119
}
120

121
func (r *AliasReconciler) create(ctx context.Context, alias *operatorv1alpha1.Alias) (ctrl.Result, error) {
1✔
122
        logr := log.FromContext(ctx)
1✔
123

1✔
124
        retry, err := r.createAlias(ctx, alias)
1✔
125
        if err != nil {
2✔
126
                meta.SetStatusCondition(&alias.Status.Conditions, getAliasReadyCondition(metav1.ConditionFalse, "Error", err.Error()))
1✔
127
                if retry {
2✔
128
                        logr.Info(fmt.Errorf("failed to create alias, requeueing: %w", err).Error())
1✔
129
                        return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
1✔
130
                }
1✔
131
                logr.Error(err, "failed to create alias")
×
132
                return ctrl.Result{}, err
×
133
        }
134

135
        if retry {
2✔
136
                return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
1✔
137
        }
1✔
138

139
        meta.SetStatusCondition(&alias.Status.Conditions, getAliasReadyCondition(metav1.ConditionTrue, "Created", "Alias created in MailU"))
1✔
140
        logr.Info("created alias")
1✔
141

1✔
142
        return ctrl.Result{}, nil
1✔
143
}
144

145
func (r *AliasReconciler) update(ctx context.Context, alias *operatorv1alpha1.Alias, apiAlias *mailu.Alias) (ctrl.Result, error) {
1✔
146
        logr := log.FromContext(ctx)
1✔
147

1✔
148
        newAlias := mailu.Alias{
1✔
149
                Email:       alias.Spec.Name + "@" + alias.Spec.Domain,
1✔
150
                Comment:     &alias.Spec.Comment,
1✔
151
                Destination: &alias.Spec.Destination,
1✔
152
                Wildcard:    &alias.Spec.Wildcard,
1✔
153
        }
1✔
154

1✔
155
        jsonNew, _ := json.Marshal(newAlias) //nolint:errcheck
1✔
156
        jsonOld, _ := json.Marshal(apiAlias) //nolint:errcheck
1✔
157

1✔
158
        if reflect.DeepEqual(jsonNew, jsonOld) {
2✔
159
                meta.SetStatusCondition(&alias.Status.Conditions, getAliasReadyCondition(metav1.ConditionTrue, "Updated", "Alias updated in MailU"))
1✔
160
                logr.Info("alias is up to date, no change needed")
1✔
161
                return ctrl.Result{}, nil
1✔
162
        }
1✔
163

164
        retry, err := r.updateAlias(ctx, newAlias)
1✔
165
        if err != nil {
2✔
166
                meta.SetStatusCondition(&alias.Status.Conditions, getAliasReadyCondition(metav1.ConditionFalse, "Error", err.Error()))
1✔
167
                if retry {
2✔
168
                        logr.Info(fmt.Errorf("failed to update alias, requeueing: %w", err).Error())
1✔
169
                        return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
1✔
170
                }
1✔
171
                logr.Error(err, "failed to update alias")
×
172
                return ctrl.Result{}, err
×
173
        }
174

175
        if retry {
1✔
NEW
176
                return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
×
UNCOV
177
        }
×
178

179
        logr.Info("updated alias")
1✔
180
        meta.SetStatusCondition(&alias.Status.Conditions, getAliasReadyCondition(metav1.ConditionTrue, "Updated", "Alias updated in MailU"))
1✔
181

1✔
182
        return ctrl.Result{}, nil
1✔
183
}
184

185
func (r *AliasReconciler) delete(ctx context.Context, alias *operatorv1alpha1.Alias) (ctrl.Result, error) {
1✔
186
        logr := log.FromContext(ctx)
1✔
187

1✔
188
        retry, err := r.deleteAlias(ctx, alias)
1✔
189
        if err != nil {
2✔
190
                meta.SetStatusCondition(&alias.Status.Conditions, getAliasReadyCondition(metav1.ConditionFalse, "Error", err.Error()))
1✔
191
                if retry {
2✔
192
                        logr.Info(fmt.Errorf("failed to delete alias, requeueing: %w", err).Error())
1✔
193
                        return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
1✔
194
                }
1✔
195
                logr.Error(err, "failed to delete alias")
×
196
                return ctrl.Result{}, err
×
197
        }
198

199
        if retry {
1✔
NEW
200
                return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
×
UNCOV
201
        }
×
202

203
        logr.Info("deleted alias")
1✔
204

1✔
205
        return ctrl.Result{}, nil
1✔
206
}
207

208
func (r *AliasReconciler) getAlias(ctx context.Context, alias *operatorv1alpha1.Alias) (*mailu.Alias, bool, error) {
1✔
209
        found, err := r.ApiClient.FindAlias(ctx, alias.Spec.Name+"@"+alias.Spec.Domain)
1✔
210
        if err != nil {
1✔
211
                return nil, false, err
×
212
        }
×
213
        defer found.Body.Close() //nolint:errcheck
1✔
214

1✔
215
        body, err := io.ReadAll(found.Body)
1✔
216
        if err != nil {
1✔
217
                return nil, true, err
×
218
        }
×
219

220
        switch found.StatusCode {
1✔
221
        case http.StatusOK:
1✔
222
                foundAlias := &mailu.Alias{}
1✔
223
                err = json.Unmarshal(body, &foundAlias)
1✔
224
                if err != nil {
1✔
225
                        return nil, true, err
×
226
                }
×
227

228
                return foundAlias, false, nil
1✔
229
        case http.StatusNotFound:
1✔
230
                return nil, false, nil
1✔
231
        case http.StatusBadRequest:
1✔
232
                return nil, false, errors.New("bad request")
1✔
233
        case http.StatusBadGateway:
×
234
                fallthrough
×
235
        case http.StatusGatewayTimeout:
×
236
                return nil, true, errors.New("gateway timeout")
×
237
        case http.StatusServiceUnavailable:
1✔
238
                return nil, true, errors.New("service unavailable")
1✔
239
        }
240
        return nil, false, errors.New("unknown status: " + strconv.Itoa(found.StatusCode))
×
241
}
242

243
func (r *AliasReconciler) createAlias(ctx context.Context, alias *operatorv1alpha1.Alias) (bool, error) {
1✔
244
        res, err := r.ApiClient.CreateAlias(ctx, mailu.Alias{
1✔
245
                Email:       alias.Spec.Name + "@" + alias.Spec.Domain,
1✔
246
                Comment:     &alias.Spec.Comment,
1✔
247
                Destination: &alias.Spec.Destination,
1✔
248
                Wildcard:    &alias.Spec.Wildcard,
1✔
249
        })
1✔
250
        if err != nil {
1✔
251
                return false, err
×
252
        }
×
253
        defer res.Body.Close() //nolint:errcheck
1✔
254

1✔
255
        _, err = io.ReadAll(res.Body)
1✔
256
        if err != nil {
1✔
257
                return false, err
×
258
        }
×
259
        switch res.StatusCode {
1✔
260
        case http.StatusCreated:
×
261
                fallthrough
×
262
        case http.StatusOK:
1✔
263
                return false, nil
1✔
264
        case http.StatusConflict:
1✔
265
                // treat conflict as success -> requeue will trigger an update
1✔
266
                return true, nil
1✔
267
        case http.StatusInternalServerError:
×
268
                return false, errors.New("internal server error")
×
269
        case http.StatusBadGateway:
×
270
                fallthrough
×
271
        case http.StatusGatewayTimeout:
×
272
                return true, errors.New("gateway timeout")
×
273
        case http.StatusServiceUnavailable:
1✔
274
                return true, errors.New("service unavailable")
1✔
275
        }
276

277
        return false, errors.New("unknown status: " + strconv.Itoa(res.StatusCode))
×
278
}
279

280
func (r *AliasReconciler) updateAlias(ctx context.Context, newAlias mailu.Alias) (bool, error) {
1✔
281
        res, err := r.ApiClient.UpdateAlias(ctx, newAlias.Email, newAlias)
1✔
282
        if err != nil {
1✔
283
                return false, err
×
284
        }
×
285
        defer res.Body.Close() //nolint:errcheck
1✔
286

1✔
287
        _, err = io.ReadAll(res.Body)
1✔
288
        if err != nil {
1✔
289
                return false, err
×
290
        }
×
291

292
        switch res.StatusCode {
1✔
293
        case http.StatusNoContent:
×
294
                fallthrough
×
295
        case http.StatusOK:
1✔
296
                return false, nil
1✔
297
        case http.StatusInternalServerError:
×
298
                return false, errors.New("internal server error")
×
299
        case http.StatusBadGateway:
×
300
                fallthrough
×
301
        case http.StatusGatewayTimeout:
×
302
                return true, errors.New("gateway timeout")
×
303
        case http.StatusServiceUnavailable:
1✔
304
                return true, errors.New("service unavailable")
1✔
305
        }
306

307
        return false, errors.New("unknown status: " + strconv.Itoa(res.StatusCode))
×
308
}
309

310
func (r *AliasReconciler) deleteAlias(ctx context.Context, alias *operatorv1alpha1.Alias) (bool, error) {
1✔
311
        res, err := r.ApiClient.DeleteAlias(ctx, alias.Spec.Name+"@"+alias.Spec.Domain)
1✔
312
        if err != nil {
1✔
313
                return false, err
×
314
        }
×
315
        defer res.Body.Close() //nolint:errcheck
1✔
316

1✔
317
        _, err = io.ReadAll(res.Body)
1✔
318
        if err != nil {
1✔
319
                return false, err
×
320
        }
×
321

322
        switch res.StatusCode {
1✔
323
        case http.StatusNotFound:
×
324
                fallthrough
×
325
        case http.StatusOK:
1✔
326
                return false, nil
1✔
327
        case http.StatusInternalServerError:
×
328
                return false, errors.New("internal server error")
×
329
        case http.StatusBadGateway:
×
330
                fallthrough
×
331
        case http.StatusGatewayTimeout:
×
332
                return true, errors.New("gateway timeout")
×
333
        case http.StatusServiceUnavailable:
1✔
334
                return true, errors.New("service unavailable")
1✔
335
        }
336

337
        return false, errors.New("unknown status: " + strconv.Itoa(res.StatusCode))
×
338
}
339

340
func getAliasReadyCondition(status metav1.ConditionStatus, reason, message string) metav1.Condition {
1✔
341
        return metav1.Condition{
1✔
342
                Type:    AliasConditionTypeReady,
1✔
343
                Status:  status,
1✔
344
                Reason:  reason,
1✔
345
                Message: message,
1✔
346
        }
1✔
347
}
1✔
348

349
// SetupWithManager sets up the controller with the Manager.
350
func (r *AliasReconciler) SetupWithManager(mgr ctrl.Manager) error {
×
351
        return ctrl.NewControllerManagedBy(mgr).
×
352
                For(&operatorv1alpha1.Alias{}).
×
353
                Complete(reconcile.AsReconciler(r.Client, r))
×
354
}
×
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