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

DoodleScheduling / webhook-controller / 24931673311

25 Apr 2026 01:10PM UTC coverage: 42.164% (-0.4%) from 42.58%
24931673311

push

github

web-flow
chore(deps-dev): update aquasecurity/trivy-action action to v0.36.0 (#500)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

304 of 721 relevant lines covered (42.16%)

3.27 hits per line

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

76.38
/internal/controllers/receiver_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
// +kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch
18
// +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch
19
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
20
// +kubebuilder:rbac:groups=webhook.infra.doodle.com,resources=receivers,verbs=get;list;watch;create;update;patch;delete
21
// +kubebuilder:rbac:groups=webhook.infra.doodle.com,resources=receivers/status,verbs=get;update;patch
22

23
package controllers
24

25
import (
26
        "cmp"
27
        "context"
28
        "fmt"
29
        "math/rand"
30
        "slices"
31

32
        "github.com/go-logr/logr"
33
        v1 "k8s.io/api/core/v1"
34
        "k8s.io/apimachinery/pkg/api/errors"
35
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
36
        "k8s.io/apimachinery/pkg/labels"
37
        "k8s.io/client-go/tools/record"
38
        ctrl "sigs.k8s.io/controller-runtime"
39
        "sigs.k8s.io/controller-runtime/pkg/client"
40
        "sigs.k8s.io/controller-runtime/pkg/controller"
41
        "sigs.k8s.io/controller-runtime/pkg/handler"
42
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
43

44
        v1beta1 "github.com/DoodleScheduling/webhook-controller/api/v1beta1"
45
        "github.com/DoodleScheduling/webhook-controller/internal/proxy"
46
)
47

48
// Receiver reconciles a Receiver object
49
type ReceiverReconciler struct {
50
        client.Client
51
        HttpProxy pathUpdater
52
        Log       logr.Logger
53
        Recorder  record.EventRecorder
54
}
55

56
type pathUpdater interface {
57
        RegisterOrUpdate(receiver proxy.Receiver) error
58
        Unregister(path string) error
59
}
60

61
type ReceiverReconcilerOptions struct {
62
        MaxConcurrentReconciles int
63
}
64

65
// SetupWithManager adding controllers
66
func (r *ReceiverReconciler) SetupWithManager(mgr ctrl.Manager, opts ReceiverReconcilerOptions) error {
1✔
67
        return ctrl.NewControllerManagedBy(mgr).
1✔
68
                For(&v1beta1.Receiver{}).
1✔
69
                Watches(
1✔
70
                        &v1.Service{},
1✔
71
                        handler.EnqueueRequestsFromMapFunc(r.requestsForChangeBySelector),
1✔
72
                ).
1✔
73
                WithOptions(controller.Options{MaxConcurrentReconciles: opts.MaxConcurrentReconciles}).
1✔
74
                Complete(r)
1✔
75
}
1✔
76

77
func (r *ReceiverReconciler) requestsForChangeBySelector(ctx context.Context, o client.Object) []reconcile.Request {
3✔
78
        svc, ok := o.(*v1.Service)
3✔
79
        if !ok {
3✔
80
                panic(fmt.Sprintf("expected a Service, got %T", o))
×
81
        }
82

83
        var ns v1.Namespace
3✔
84
        if err := r.Get(ctx, client.ObjectKey{Name: svc.Namespace}, &ns); err != nil {
3✔
85
                if errors.IsNotFound(err) {
×
86
                        return nil
×
87
                }
×
88
        }
89

90
        var list v1beta1.ReceiverList
3✔
91
        if err := r.List(ctx, &list); err != nil {
3✔
92
                return nil
×
93
        }
×
94

95
        var reqs []reconcile.Request
3✔
96
        for _, receiver := range list.Items {
7✔
97
                receiver := receiver
4✔
98
                if receiver.Spec.Targets == nil {
4✔
99
                        continue
×
100
                }
101

102
                for _, target := range receiver.Spec.Targets {
6✔
103
                        if target.Service.Name != svc.Name {
2✔
104
                                continue
×
105
                        }
106

107
                        if target.NamespaceSelector == nil {
4✔
108
                                if receiver.Namespace == svc.Namespace {
4✔
109
                                        r.Log.V(1).Info("referenced resource from a Receiver changed detected", "namespace", receiver.Namespace, "receiver-name", receiver.Name)
2✔
110
                                        reqs = append(reqs, reconcile.Request{NamespacedName: objectKey(&receiver)})
2✔
111
                                }
2✔
112
                        } else {
×
113
                                labelSel, err := metav1.LabelSelectorAsSelector(target.NamespaceSelector)
×
114
                                if err != nil {
×
115
                                        r.Log.Error(err, "can not select resourceSelector selectors")
×
116
                                        continue
×
117
                                }
118

119
                                if labelSel.Matches(labels.Set(ns.GetLabels())) {
×
120
                                        r.Log.V(1).Info("referenced resource from a Receiver changed detected", "namespace", receiver.Namespace, "receiver-name", receiver.Name)
×
121
                                        reqs = append(reqs, reconcile.Request{NamespacedName: objectKey(&receiver)})
×
122
                                }
×
123
                        }
124
                }
125
        }
126

127
        return reqs
3✔
128
}
129

130
var chars = []rune("abcdefghijklmnopqrstuvwxyz123456789")
131

132
func randSeq(n int) string {
2✔
133
        b := make([]rune, n)
2✔
134
        for i := range b {
66✔
135
                b[i] = chars[rand.Intn(len(chars))]
64✔
136
        }
64✔
137
        return string(b)
2✔
138
}
139

140
// Reconcile Receivers
141
func (r *ReceiverReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
9✔
142
        logger := r.Log.WithValues("Namespace", req.Namespace, "Name", req.NamespacedName)
9✔
143
        logger.Info("reconciling Receiver")
9✔
144

9✔
145
        // Fetch the Receiver instance
9✔
146
        receiver := v1beta1.Receiver{}
9✔
147

9✔
148
        err := r.Get(ctx, req.NamespacedName, &receiver)
9✔
149
        if err != nil {
10✔
150
                if errors.IsNotFound(err) {
2✔
151
                        // Request object not found, could have been deleted after reconcile request.
1✔
152
                        // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers.
1✔
153
                        // Return and don't requeue
1✔
154
                        return reconcile.Result{}, nil
1✔
155
                }
1✔
156
                // Error reading the object - requeue the request.
157
                return reconcile.Result{}, err
×
158
        }
159

160
        if receiver.Spec.Suspend {
9✔
161
                return ctrl.Result{}, nil
1✔
162
        }
1✔
163

164
        receiver.Status.ObservedGeneration = receiver.Generation
7✔
165
        receiver, result, reconcileErr := r.reconcile(ctx, receiver, logger)
7✔
166

7✔
167
        // Update status after reconciliation.
7✔
168
        if err = r.patchStatus(ctx, &receiver); err != nil {
7✔
169
                logger.Error(err, "unable to update status after reconciliation")
×
170
                return ctrl.Result{Requeue: true}, err
×
171
        }
×
172

173
        return result, reconcileErr
7✔
174
}
175

176
func (r *ReceiverReconciler) reconcile(ctx context.Context, receiver v1beta1.Receiver, logger logr.Logger) (v1beta1.Receiver, ctrl.Result, error) {
7✔
177
        receiver, services, err := r.extendWithTargets(ctx, receiver, logger)
7✔
178
        if err != nil {
7✔
179
                return receiver, ctrl.Result{}, err
×
180
        }
×
181

182
        if receiver.Status.WebhookPath == "" {
9✔
183
                receiver.Status.WebhookPath = fmt.Sprintf("/hooks/%s", randSeq(32))
2✔
184
        }
2✔
185

186
        var targets []proxy.Target
7✔
187

7✔
188
        for _, svc := range services {
9✔
189
                targets = append(targets, proxy.Target{
2✔
190
                        Address:          svc.addr,
2✔
191
                        ResponseType:     proxy.ResponseType(receiver.Spec.ResponseType),
2✔
192
                        Port:             svc.port,
2✔
193
                        ServiceName:      svc.ref.Name,
2✔
194
                        ServiceNamespace: svc.ref.Namespace,
2✔
195
                        Path:             svc.path,
2✔
196
                })
2✔
197
        }
2✔
198

199
        if len(targets) == 0 {
12✔
200
                if err := r.HttpProxy.Unregister(receiver.Status.WebhookPath); err != nil {
5✔
201
                        return receiver, ctrl.Result{}, err
×
202
                }
×
203

204
                msg := "no targets found"
5✔
205
                r.Recorder.Event(&receiver, "Normal", "info", msg)
5✔
206
                return v1beta1.ReceiverNotReady(receiver, v1beta1.ServiceBackendReadyReason, msg), ctrl.Result{}, nil
5✔
207
        }
208

209
        err = r.HttpProxy.RegisterOrUpdate(proxy.Receiver{
2✔
210
                Timeout:       receiver.Spec.Timeout.Duration,
2✔
211
                Path:          receiver.Status.WebhookPath,
2✔
212
                Targets:       targets,
2✔
213
                ResponseType:  proxy.ResponseType(receiver.Spec.ResponseType),
2✔
214
                BodySizeLimit: receiver.Spec.BodySizeLimit,
2✔
215
        })
2✔
216

2✔
217
        if err != nil {
2✔
218
                return v1beta1.Receiver{}, ctrl.Result{}, err
×
219
        }
×
220

221
        msg := "receiver successfully registered"
2✔
222
        r.Recorder.Event(&receiver, "Normal", "info", msg)
2✔
223
        return v1beta1.ReceiverReady(receiver, v1beta1.ServiceBackendReadyReason, msg), ctrl.Result{}, err
2✔
224
}
225

226
type targetService struct {
227
        addr string
228
        port int32
229
        path string
230
        ref  v1beta1.ResourceReference
231
}
232

233
func (r *ReceiverReconciler) extendWithTargets(ctx context.Context, receiver v1beta1.Receiver, logger logr.Logger) (v1beta1.Receiver, []targetService, error) {
7✔
234
        var services []targetService
7✔
235

7✔
236
        receiver.Status.SubResourceCatalog = []v1beta1.ResourceReference{}
7✔
237

7✔
238
        for _, target := range receiver.Spec.Targets {
12✔
239
                var namespaces v1.NamespaceList
5✔
240
                if target.NamespaceSelector == nil {
10✔
241
                        namespaces.Items = append(namespaces.Items, v1.Namespace{
5✔
242
                                ObjectMeta: metav1.ObjectMeta{
5✔
243
                                        Name: receiver.Namespace,
5✔
244
                                },
5✔
245
                        })
5✔
246
                } else {
5✔
247
                        namespaceSelector, err := metav1.LabelSelectorAsSelector(target.NamespaceSelector)
×
248
                        if err != nil {
×
249
                                return receiver, nil, err
×
250
                        }
×
251

252
                        err = r.List(ctx, &namespaces, client.MatchingLabelsSelector{Selector: namespaceSelector})
×
253
                        if err != nil {
×
254
                                return receiver, nil, err
×
255
                        }
×
256
                }
257

258
                for _, namespace := range namespaces.Items {
10✔
259
                        service := v1.Service{}
5✔
260
                        err := r.Get(ctx, client.ObjectKey{
5✔
261
                                Namespace: namespace.Name,
5✔
262
                                Name:      target.Service.Name,
5✔
263
                        }, &service)
5✔
264

5✔
265
                        if err != nil {
8✔
266
                                logger.V(1).Error(err, "no service found for target", "namespace", namespace.Name, "service", target.Service.Name)
3✔
267
                                continue
3✔
268
                        }
269

270
                        var port int32
2✔
271
                        for _, p := range service.Spec.Ports {
4✔
272
                                if target.Service.Port.Name != nil && p.Name == *target.Service.Port.Name {
4✔
273
                                        port = p.Port
2✔
274
                                } else if target.Service.Port.Number != nil && p.Port == *target.Service.Port.Number {
2✔
275
                                        port = p.Port
×
276
                                }
×
277
                        }
278

279
                        if port == 0 {
2✔
280
                                logger.V(1).Error(err, "port not found for target", "namespace", namespace.Name, "service", target.Service.Name)
×
281
                                continue
×
282
                        }
283

284
                        if service.Spec.ClusterIP == "" {
2✔
285
                                continue
×
286
                        }
287

288
                        services = append(services, targetService{
2✔
289
                                addr: service.Spec.ClusterIP,
2✔
290
                                path: target.Path,
2✔
291
                                port: port,
2✔
292
                                ref: v1beta1.ResourceReference{
2✔
293
                                        Kind:       service.Kind,
2✔
294
                                        Name:       service.Name,
2✔
295
                                        Namespace:  service.Namespace,
2✔
296
                                        APIVersion: service.APIVersion,
2✔
297
                                },
2✔
298
                        })
2✔
299
                }
300
        }
301

302
        slices.SortFunc(services, func(a, b targetService) int {
7✔
303
                return cmp.Or(
×
304
                        cmp.Compare(a.ref.Name, b.ref.Name),
×
305
                        cmp.Compare(a.ref.Namespace, b.ref.Namespace),
×
306
                )
×
307
        })
×
308

309
        for _, svc := range services {
9✔
310
                receiver.Status.SubResourceCatalog = append(receiver.Status.SubResourceCatalog, svc.ref)
2✔
311
        }
2✔
312

313
        return receiver, services, nil
7✔
314
}
315

316
func (r *ReceiverReconciler) patchStatus(ctx context.Context, receiver *v1beta1.Receiver) error {
7✔
317
        key := client.ObjectKeyFromObject(receiver)
7✔
318
        latest := &v1beta1.Receiver{}
7✔
319
        if err := r.Get(ctx, key, latest); err != nil {
7✔
320
                return err
×
321
        }
×
322

323
        return r.Status().Patch(ctx, receiver, client.MergeFrom(latest))
7✔
324
}
325

326
// objectKey returns client.ObjectKey for the object.
327
func objectKey(object metav1.Object) client.ObjectKey {
2✔
328
        return client.ObjectKey{
2✔
329
                Namespace: object.GetNamespace(),
2✔
330
                Name:      object.GetName(),
2✔
331
        }
2✔
332
}
2✔
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