/
predicates.go
795 lines (724 loc) · 32 KB
/
predicates.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
package test
import (
"context"
"io"
"net/http"
"slices"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
appsv1 "k8s.io/api/apps/v1"
autoscalingv2 "k8s.io/api/autoscaling/v2"
corev1 "k8s.io/api/core/v1"
netv1 "k8s.io/api/networking/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
operatorv1beta1 "github.com/kong/gateway-operator/apis/v1beta1"
"github.com/kong/gateway-operator/controllers/controlplane"
gwtypes "github.com/kong/gateway-operator/internal/types"
"github.com/kong/gateway-operator/pkg/clientset"
"github.com/kong/gateway-operator/pkg/consts"
gatewayutils "github.com/kong/gateway-operator/pkg/utils/gateway"
k8sutils "github.com/kong/gateway-operator/pkg/utils/kubernetes"
k8sresources "github.com/kong/gateway-operator/pkg/utils/kubernetes/resources"
)
// controlPlanePredicate is a helper function for tests that returns a function
// that can be used to check if a ControlPlane has a certain state.
func controlPlanePredicate(
t *testing.T,
ctx context.Context,
controlplaneName types.NamespacedName,
predicate func(controlplane *operatorv1beta1.ControlPlane) bool,
operatorClient *clientset.Clientset,
) func() bool {
controlplaneClient := operatorClient.ApisV1beta1().ControlPlanes(controlplaneName.Namespace)
return func() bool {
controlplane, err := controlplaneClient.Get(ctx, controlplaneName.Name, metav1.GetOptions{})
require.NoError(t, err)
return predicate(controlplane)
}
}
// DataPlanePredicate is a helper function for tests that returns a function
// that can be used to check if a DataPlane has a certain state.
func DataPlanePredicate(
t *testing.T,
ctx context.Context,
dataplaneName types.NamespacedName,
predicate func(dataplane *operatorv1beta1.DataPlane) bool,
operatorClient *clientset.Clientset,
) func() bool {
dataPlaneClient := operatorClient.ApisV1beta1().DataPlanes(dataplaneName.Namespace)
return func() bool {
dataplane, err := dataPlaneClient.Get(ctx, dataplaneName.Name, metav1.GetOptions{})
require.NoError(t, err)
return predicate(dataplane)
}
}
// HPAPredicate is a helper function for tests that returns a function
// that can be used to check if an HPA has a certain state.
func HPAPredicate(
t *testing.T,
ctx context.Context,
hpaName types.NamespacedName,
predicate func(hpa *autoscalingv2.HorizontalPodAutoscaler) bool,
client client.Client,
) func() bool {
return func() bool {
var hpa autoscalingv2.HorizontalPodAutoscaler
require.NoError(t, client.Get(ctx, hpaName, &hpa))
return predicate(&hpa)
}
}
// ControlPlaneIsScheduled is a helper function for tests that returns a function
// that can be used to check if a ControlPlane was scheduled.
// Should be used in conjunction with require.Eventually or assert.Eventually.
func ControlPlaneIsScheduled(t *testing.T, ctx context.Context, controlPlane types.NamespacedName, operatorClient *clientset.Clientset) func() bool {
return controlPlanePredicate(t, ctx, controlPlane, func(c *operatorv1beta1.ControlPlane) bool {
for _, condition := range c.Status.Conditions {
if condition.Type == string(controlplane.ConditionTypeProvisioned) {
return true
}
}
return false
}, operatorClient)
}
// DataPlaneIsReady is a helper function for tests that returns a function
// that can be used to check if a DataPlane is ready.
// Should be used in conjunction with require.Eventually or assert.Eventually.
func DataPlaneIsReady(t *testing.T, ctx context.Context, dataplane types.NamespacedName, operatorClient *clientset.Clientset) func() bool {
return DataPlanePredicate(t, ctx, dataplane, func(c *operatorv1beta1.DataPlane) bool {
for _, condition := range c.Status.Conditions {
if condition.Type == string(k8sutils.ReadyType) && condition.Status == metav1.ConditionTrue {
return true
}
}
return false
}, operatorClient)
}
// ControlPlaneDetectedNoDataPlane is a helper function for tests that returns a function
// that can be used to check if a ControlPlane detected unset dataplane.
// Should be used in conjunction with require.Eventually or assert.Eventually.
func ControlPlaneDetectedNoDataPlane(t *testing.T, ctx context.Context, controlPlane types.NamespacedName, clients K8sClients) func() bool {
return controlPlanePredicate(t, ctx, controlPlane, func(c *operatorv1beta1.ControlPlane) bool {
for _, condition := range c.Status.Conditions {
if condition.Type == string(controlplane.ConditionTypeProvisioned) &&
condition.Status == metav1.ConditionFalse &&
condition.Reason == string(controlplane.ConditionReasonNoDataPlane) {
return true
}
}
return false
}, clients.OperatorClient)
}
// ControlPlaneIsProvisioned is a helper function for tests that returns a function
// that can be used to check if a ControlPlane was provisioned.
// Should be used in conjunction with require.Eventually or assert.Eventually.
func ControlPlaneIsProvisioned(t *testing.T, ctx context.Context, controlPlane types.NamespacedName, clients K8sClients) func() bool {
return controlPlanePredicate(t, ctx, controlPlane, func(c *operatorv1beta1.ControlPlane) bool {
for _, condition := range c.Status.Conditions {
if condition.Type == string(controlplane.ConditionTypeProvisioned) &&
condition.Status == metav1.ConditionTrue {
return true
}
}
return false
}, clients.OperatorClient)
}
// ControlPlaneIsNotReady is a helper function for tests. It returns a function
// that can be used to check if a ControlPlane is marked as not Ready.
// Should be used in conjunction with require.Eventually or assert.Eventually.
func ControlPlaneIsNotReady(t *testing.T, ctx context.Context, controlplane types.NamespacedName, clients K8sClients) func() bool {
return controlPlanePredicate(t, ctx, controlplane, func(c *operatorv1beta1.ControlPlane) bool {
for _, condition := range c.Status.Conditions {
if condition.Type == string(k8sutils.ReadyType) &&
condition.Status == metav1.ConditionFalse {
return true
}
}
return false
}, clients.OperatorClient)
}
// ControlPlaneIsReady is a helper function for tests. It returns a function
// that can be used to check if a ControlPlane is marked as Ready.
// Should be used in conjunction with require.Eventually or assert.Eventually.
func ControlPlaneIsReady(t *testing.T, ctx context.Context, controlplane types.NamespacedName, clients K8sClients) func() bool {
return controlPlanePredicate(t, ctx, controlplane, func(c *operatorv1beta1.ControlPlane) bool {
for _, condition := range c.Status.Conditions {
if condition.Type == string(k8sutils.ReadyType) &&
condition.Status == metav1.ConditionTrue {
return true
}
}
return false
}, clients.OperatorClient)
}
// ControlPlaneHasActiveDeployment is a helper function for tests that returns a function
// that can be used to check if a ControlPlane has an active deployment.
// Should be used in conjunction with require.Eventually or assert.Eventually.
func ControlPlaneHasActiveDeployment(t *testing.T, ctx context.Context, controlplaneName types.NamespacedName, clients K8sClients) func() bool {
return controlPlanePredicate(t, ctx, controlplaneName, func(controlplane *operatorv1beta1.ControlPlane) bool {
deployments := MustListControlPlaneDeployments(t, ctx, controlplane, clients)
return len(deployments) == 1 &&
*deployments[0].Spec.Replicas > 0 &&
deployments[0].Status.AvailableReplicas == *deployments[0].Spec.Replicas
}, clients.OperatorClient)
}
// ControlPlaneHasClusterRole is a helper function for tests that returns a function
// that can be used to check if a ControlPlane has a ClusterRole.
// Should be used in conjunction with require.Eventually or assert.Eventually.
func ControlPlaneHasClusterRole(t *testing.T, ctx context.Context, controlplane *operatorv1beta1.ControlPlane, clients K8sClients) func() bool {
return func() bool {
clusterRoles := MustListControlPlaneClusterRoles(t, ctx, controlplane, clients)
t.Logf("%d clusterroles", len(clusterRoles))
return len(clusterRoles) > 0
}
}
// ControlPlanesClusterRoleHasPolicyRule is a helper function for tests that returns a function
// that can be used to check if ControlPlane's ClusterRole contains the provided PolicyRule.
// Should be used in conjunction with require.Eventually or assert.Eventually.
func ControlPlanesClusterRoleHasPolicyRule(t *testing.T, ctx context.Context, controlplane *operatorv1beta1.ControlPlane, clients K8sClients, pr rbacv1.PolicyRule) func() bool {
return func() bool {
clusterRoles := MustListControlPlaneClusterRoles(t, ctx, controlplane, clients)
t.Logf("%d clusterroles", len(clusterRoles))
if len(clusterRoles) == 0 {
return false
}
t.Logf("got %s clusterrole, checking if it contains the requested PolicyRule", clusterRoles[0].Name)
return slices.ContainsFunc(clusterRoles[0].Rules, func(e rbacv1.PolicyRule) bool {
return slices.Equal(e.APIGroups, pr.APIGroups) &&
slices.Equal(e.ResourceNames, pr.ResourceNames) &&
slices.Equal(e.Resources, pr.Resources) &&
slices.Equal(e.Verbs, pr.Verbs) &&
slices.Equal(e.NonResourceURLs, pr.NonResourceURLs)
})
}
}
// ControlPlanesClusterRoleBindingHasSubject is a helper function for tests that returns a function
// that can be used to check if ControlPlane's ClusterRoleBinding contains the provided Subject.
// Should be used in conjunction with require.Eventually or assert.Eventually.
func ControlPlanesClusterRoleBindingHasSubject(t *testing.T, ctx context.Context, controlplane *operatorv1beta1.ControlPlane, clients K8sClients, subject rbacv1.Subject) func() bool {
return func() bool {
clusterRoleBindings := MustListControlPlaneClusterRoleBindings(t, ctx, controlplane, clients)
t.Logf("%d clusterrolesbindings", len(clusterRoleBindings))
if len(clusterRoleBindings) == 0 {
return false
}
t.Logf("got %s clusterrolebinding, checking if it contains the requested Subject", clusterRoleBindings[0].Name)
return slices.ContainsFunc(clusterRoleBindings[0].Subjects, func(e rbacv1.Subject) bool {
return e.Kind == subject.Kind &&
e.APIGroup == subject.APIGroup &&
e.Name == subject.Name &&
e.Namespace == subject.Namespace
})
}
}
// ControlPlaneHasClusterRoleBinding is a helper function for tests that returns a function
// that can be used to check if a ControlPlane has a ClusterRoleBinding.
// Should be used in conjunction with require.Eventually or assert.Eventually.
func ControlPlaneHasClusterRoleBinding(t *testing.T, ctx context.Context, controlplane *operatorv1beta1.ControlPlane, clients K8sClients) func() bool {
return func() bool {
clusterRoleBindings := MustListControlPlaneClusterRoleBindings(t, ctx, controlplane, clients)
t.Logf("%d clusterrolebindings", len(clusterRoleBindings))
return len(clusterRoleBindings) > 0
}
}
// ControlPlaneCRBContainsCRAndSA is a helper function for tests that returns a function
// that can be used to check if the ClusterRoleBinding of a ControPlane has the reference of ClusterRole belonging to the ControlPlane
// and contains the service account used by the Deployment of the ControlPlane.
func ControlPlaneCRBContainsCRAndSA(t *testing.T, ctx context.Context, controlplane *operatorv1beta1.ControlPlane, clients K8sClients) func() bool {
return func() bool {
clusterRoleBindings := MustListControlPlaneClusterRoleBindings(t, ctx, controlplane, clients)
clusterRoles := MustListControlPlaneClusterRoles(t, ctx, controlplane, clients)
deployments := MustListControlPlaneDeployments(t, ctx, controlplane, clients)
if len(clusterRoleBindings) != 1 || len(clusterRoles) != 1 || len(deployments) != 1 {
return false
}
clusterRoleBinding := clusterRoleBindings[0]
clusterRole := clusterRoles[0]
serviceAccountName := deployments[0].Spec.Template.Spec.ServiceAccountName
return k8sresources.CompareClusterRoleName(&clusterRoleBinding, clusterRole.Name) &&
k8sresources.ClusterRoleBindingContainsServiceAccount(&clusterRoleBinding, controlplane.Namespace, serviceAccountName)
}
}
// ControlPlaneHasNReadyPods checks if a ControlPlane has at least N ready Pods.
func ControlPlaneHasNReadyPods(t *testing.T, ctx context.Context, controlplaneName types.NamespacedName, clients K8sClients, n int) func() bool {
return controlPlanePredicate(t, ctx, controlplaneName, func(controlplane *operatorv1beta1.ControlPlane) bool {
deployments := MustListControlPlaneDeployments(t, ctx, controlplane, clients)
return len(deployments) == 1 &&
*deployments[0].Spec.Replicas == int32(n) &&
deployments[0].Status.AvailableReplicas == *deployments[0].Spec.Replicas
}, clients.OperatorClient)
}
// ControlPlaneHasAdmissionWebhookService is a helper function for tests that returns a function
// that can be used to check if a ControlPlane has an admission webhook Service.
// Should be used in conjunction with require.Eventually or assert.Eventually.
func ControlPlaneHasAdmissionWebhookService(t *testing.T, ctx context.Context, cp *operatorv1beta1.ControlPlane, clients K8sClients) func() bool {
return func() bool {
services, err := k8sutils.ListServicesForOwner(ctx, clients.MgrClient, cp.Namespace, cp.UID, client.MatchingLabels{
consts.ControlPlaneServiceLabel: consts.ControlPlaneServiceKindWebhook,
})
require.NoError(t, err)
t.Logf("%d webhook services", len(services))
return len(services) > 0
}
}
// ControlPlaneHasAdmissionWebhookCertificateSecret is a helper function for tests that returns a function
// that can be used to check if a ControlPlane has an admission webhook certificate Secret.
// Should be used in conjunction with require.Eventually or assert.Eventually.
func ControlPlaneHasAdmissionWebhookCertificateSecret(t *testing.T, ctx context.Context, cp *operatorv1beta1.ControlPlane, clients K8sClients) func() bool {
return func() bool {
services, err := k8sutils.ListSecretsForOwner(ctx, clients.MgrClient, cp.UID, client.MatchingLabels{
consts.SecretUsedByServiceLabel: consts.ControlPlaneServiceKindWebhook,
})
require.NoError(t, err)
t.Logf("%d webhook secrets", len(services))
return len(services) > 0
}
}
// ControlPlaneHasAdmissionWebhookConfiguration is a helper function for tests that returns a function
// that can be used to check if a ControlPlane has an admission webhook configuration.
func ControlPlaneHasAdmissionWebhookConfiguration(t *testing.T, ctx context.Context, cp *operatorv1beta1.ControlPlane, clients K8sClients) func() bool {
return func() bool {
services, err := k8sutils.ListValidatingWebhookConfigurationsForOwner(ctx, clients.MgrClient, cp.UID)
require.NoError(t, err)
t.Logf("%d validating webhook configurations", len(services))
return len(services) > 0
}
}
// DataPlaneHasActiveDeployment is a helper function for tests that returns a function
// that can be used to check if a DataPlane has an active deployment (that is,
// a Deployment that has at least 1 Replica and that all Replicas as marked as Available).
// Should be used in conjunction with require.Eventually or assert.Eventually.
func DataPlaneHasActiveDeployment(
t *testing.T,
ctx context.Context,
dataplaneNN types.NamespacedName,
ret *appsv1.Deployment,
matchingLabels client.MatchingLabels,
clients K8sClients,
) func() bool {
return DataPlanePredicate(t, ctx, dataplaneNN, func(dataplane *operatorv1beta1.DataPlane) bool {
deployments := MustListDataPlaneDeployments(t, ctx, dataplane, clients, matchingLabels)
if len(deployments) == 1 &&
deployments[0].Status.AvailableReplicas == *deployments[0].Spec.Replicas {
if ret != nil {
*ret = deployments[0]
}
return true
}
return false
}, clients.OperatorClient)
}
// DataPlaneHasHPA is a helper function for tests that returns a function
// that can be used to check if a DataPlane has an active HPA.
// Should be used in conjunction with require.Eventually or assert.Eventually.
func DataPlaneHasHPA(
t *testing.T,
ctx context.Context,
dataplane *operatorv1beta1.DataPlane,
ret *autoscalingv2.HorizontalPodAutoscaler,
clients K8sClients,
) func() bool {
dataplaneName := client.ObjectKeyFromObject(dataplane)
const dataplaneDeploymentAppLabel = "app"
return DataPlanePredicate(t, ctx, dataplaneName, func(dataplane *operatorv1beta1.DataPlane) bool {
deployments := MustListDataPlaneDeployments(t, ctx, dataplane, clients, client.MatchingLabels{
dataplaneDeploymentAppLabel: dataplane.Name,
consts.GatewayOperatorManagedByLabel: consts.DataPlaneManagedLabelValue,
consts.DataPlaneDeploymentStateLabel: consts.DataPlaneStateLabelValueLive, // Only live Deployment has an HPA.
})
if len(deployments) != 1 {
return false
}
hpas := MustListDataPlaneHPAs(t, ctx, dataplane, clients, client.MatchingLabels{
dataplaneDeploymentAppLabel: dataplane.Name,
consts.GatewayOperatorManagedByLabel: consts.DataPlaneManagedLabelValue,
})
if len(hpas) != 1 {
return false
}
hpa := hpas[0]
if hpa.Spec.ScaleTargetRef.Name != deployments[0].Name {
return false
}
if ret != nil {
*ret = hpa
}
return true
}, clients.OperatorClient)
}
// DataPlaneHasDeployment is a helper function for tests that returns a function
// that can be used to check if a DataPlane has a Deployment.
// Optionally the caller can provide a list of assertions that will be checked
// against the found Deployment.
// Should be used in conjunction with require.Eventually or assert.Eventually.
func DataPlaneHasDeployment(
t *testing.T,
ctx context.Context,
dataplaneName types.NamespacedName,
ret *appsv1.Deployment,
clients K8sClients,
matchingLabels client.MatchingLabels,
asserts ...func(appsv1.Deployment) bool,
) func() bool {
return DataPlanePredicate(t, ctx, dataplaneName, func(dataplane *operatorv1beta1.DataPlane) bool {
deployments := MustListDataPlaneDeployments(t, ctx, dataplane, clients, matchingLabels)
if len(deployments) != 1 {
return false
}
deployment := deployments[0]
for _, a := range asserts {
if !a(deployment) {
return false
}
}
if ret != nil {
*ret = deployment
}
return true
}, clients.OperatorClient)
}
// DataPlaneHasNReadyPods checks if a DataPlane has at least N ready Pods.
func DataPlaneHasNReadyPods(t *testing.T, ctx context.Context, dataplaneName types.NamespacedName, clients K8sClients, n int) func() bool {
return DataPlanePredicate(t, ctx, dataplaneName, func(dataplane *operatorv1beta1.DataPlane) bool {
deployments := MustListDataPlaneDeployments(t, ctx, dataplane, clients, client.MatchingLabels{
consts.GatewayOperatorManagedByLabel: consts.DataPlaneManagedLabelValue,
})
return len(deployments) == 1 &&
*deployments[0].Spec.Replicas == int32(n) &&
deployments[0].Status.AvailableReplicas == *deployments[0].Spec.Replicas
}, clients.OperatorClient)
}
// DataPlaneHasService is a helper function for tests that returns a function
// that can be used to check if a DataPlane has a service created.
// Should be used in conjunction with require.Eventually or assert.Eventually.
func DataPlaneHasService(t *testing.T, ctx context.Context, dataplaneName types.NamespacedName, clients K8sClients, matchingLabels client.MatchingLabels) func() bool {
return DataPlanePredicate(t, ctx, dataplaneName, func(dataplane *operatorv1beta1.DataPlane) bool {
services := MustListDataPlaneServices(t, ctx, dataplane, clients.MgrClient, matchingLabels)
return len(services) == 1
}, clients.OperatorClient)
}
// DataPlaneHasActiveService is a helper function for tests that returns a function
// that can be used to check if a DataPlane has an active proxy service.
// Should be used in conjunction with require.Eventually or assert.Eventually.
func DataPlaneHasActiveService(t *testing.T, ctx context.Context, dataplaneName types.NamespacedName, ret *corev1.Service, clients K8sClients, matchingLabels client.MatchingLabels) func() bool {
return DataPlanePredicate(t, ctx, dataplaneName, func(dataplane *operatorv1beta1.DataPlane) bool {
services := MustListDataPlaneServices(t, ctx, dataplane, clients.MgrClient, matchingLabels)
if len(services) == 1 {
if ret != nil {
*ret = services[0]
}
return true
}
return false
}, clients.OperatorClient)
}
// DataPlaneServiceHasNActiveEndpoints is a helper function for tests that returns a function
// that can be used to check if a Service has active endpoints.
// Should be used in conjunction with require.Eventually or assert.Eventually.
func DataPlaneServiceHasNActiveEndpoints(t *testing.T, ctx context.Context, serviceName types.NamespacedName, clients K8sClients, n int) func() bool {
return func() bool {
endpointSlices := MustListServiceEndpointSlices(
t,
ctx,
serviceName,
clients.MgrClient,
)
if len(endpointSlices) != 1 {
return false
}
return len(endpointSlices[0].Endpoints) == n
}
}
// DataPlaneHasServiceAndAddressesInStatus is a helper function for tests that returns
// a function that can be used to check if a DataPlane has:
// - a backing service name in its .Service status field
// - a list of addreses of its backing service in its .Addresses status field
// Should be used in conjunction with require.Eventually or assert.Eventually.
func DataPlaneHasServiceAndAddressesInStatus(t *testing.T, ctx context.Context, dataplaneName types.NamespacedName, clients K8sClients) func() bool {
return DataPlanePredicate(t, ctx, dataplaneName, func(dataplane *operatorv1beta1.DataPlane) bool {
services := MustListDataPlaneServices(t, ctx, dataplane, clients.MgrClient, client.MatchingLabels{
consts.GatewayOperatorManagedByLabel: consts.DataPlaneManagedLabelValue,
consts.DataPlaneServiceTypeLabel: string(consts.DataPlaneIngressServiceLabelValue),
})
if len(services) != 1 {
return false
}
service := services[0]
if dataplane.Status.Service != service.Name {
t.Logf("DataPlane %q: found %q as backing service, wanted %q",
dataplane.Name, dataplane.Status.Service, service.Name,
)
return false
}
var wanted []string
for _, ingress := range service.Status.LoadBalancer.Ingress {
if ingress.IP != "" {
wanted = append(wanted, ingress.IP)
}
if ingress.Hostname != "" {
wanted = append(wanted, ingress.Hostname)
}
}
wanted = append(wanted, service.Spec.ClusterIPs...)
var addresses []string
for _, addr := range dataplane.Status.Addresses {
addresses = append(addresses, addr.Value)
}
if len(addresses) != len(wanted) {
t.Logf("DataPlane %q: found %d addresses %v, wanted %d %v",
dataplane.Name, len(addresses), addresses, len(wanted), wanted,
)
return false
}
if !cmp.Equal(addresses, wanted) {
t.Logf("DataPlane %q: found addresses %v, wanted %v",
dataplane.Name, addresses, wanted,
)
return false
}
return true
}, clients.OperatorClient)
}
// DataPlaneUpdateEventually is a helper function for tests that returns a function
// that can be used to update the DataPlane.
// Should be used in conjunction with require.Eventually or assert.Eventually.
func DataPlaneUpdateEventually(t *testing.T, ctx context.Context, dataplaneNN types.NamespacedName, clients K8sClients, updateFunc func(*operatorv1beta1.DataPlane)) func() bool {
return func() bool {
cl := clients.OperatorClient.ApisV1beta1().DataPlanes(dataplaneNN.Namespace)
dp, err := cl.Get(ctx, dataplaneNN.Name, metav1.GetOptions{})
if err != nil {
t.Logf("error getting dataplane: %v", err)
return false
}
updateFunc(dp)
_, err = cl.Update(ctx, dp, metav1.UpdateOptions{})
if err != nil {
t.Logf("error updating dataplane: %v", err)
return false
}
return true
}
}
// ControlPlaneUpdateEventually is a helper function for tests that returns a function
// that can be used to update the ControlPlane.
// Should be used in conjunction with require.Eventually or assert.Eventually.
func ControlPlaneUpdateEventually(t *testing.T, ctx context.Context, controlplaneNN types.NamespacedName, clients K8sClients, updateFunc func(*operatorv1beta1.ControlPlane)) func() bool {
return func() bool {
cl := clients.OperatorClient.ApisV1beta1().ControlPlanes(controlplaneNN.Namespace)
dp, err := cl.Get(ctx, controlplaneNN.Name, metav1.GetOptions{})
if err != nil {
t.Logf("error getting controlplane: %v", err)
return false
}
updateFunc(dp)
_, err = cl.Update(ctx, dp, metav1.UpdateOptions{})
if err != nil {
t.Logf("error updating controlplane: %v", err)
return false
}
return true
}
}
// DataPlaneHasServiceSecret checks if a DataPlane's Service has one owned Secret.
func DataPlaneHasServiceSecret(t *testing.T, ctx context.Context, dpNN, usingSvc types.NamespacedName, ret *corev1.Secret, clients K8sClients) func() bool {
return DataPlanePredicate(t, ctx, dpNN, func(dp *operatorv1beta1.DataPlane) bool {
secrets, err := k8sutils.ListSecretsForOwner(ctx, clients.MgrClient, dp.GetUID(), client.MatchingLabels{
consts.GatewayOperatorManagedByLabel: consts.DataPlaneManagedLabelValue,
consts.ServiceSecretLabel: usingSvc.Name,
})
if err != nil {
t.Logf("error listing secrets: %v", err)
return false
}
if len(secrets) == 1 {
*ret = secrets[0]
return true
}
return false
}, clients.OperatorClient)
}
// GatewayClassIsAccepted is a helper function for tests that returns a function
// that can be used to check if a GatewayClass is accepted.
// Should be used in conjunction with require.Eventually or assert.Eventually.
func GatewayClassIsAccepted(t *testing.T, ctx context.Context, gatewayClassName string, clients K8sClients) func() bool {
gatewayClasses := clients.GatewayClient.GatewayV1().GatewayClasses()
return func() bool {
gwc, err := gatewayClasses.Get(context.Background(), gatewayClassName, metav1.GetOptions{})
if err != nil {
return false
}
for _, cond := range gwc.Status.Conditions {
if cond.Reason == string(gatewayv1.GatewayClassConditionStatusAccepted) {
if cond.ObservedGeneration == gwc.Generation {
return true
}
}
}
return false
}
}
// GatewayNotExist is a helper function for tests that returns a function
// to check a if gateway(with specified namespace and name) does not exist.
//
// Should be used in conjunction with require.Eventually or assert.Eventually.
func GatewayNotExist(t *testing.T, ctx context.Context, gatewayNSN types.NamespacedName, clients K8sClients) func() bool {
return func() bool {
gateways := clients.GatewayClient.GatewayV1().Gateways(gatewayNSN.Namespace)
_, err := gateways.Get(ctx, gatewayNSN.Name, metav1.GetOptions{})
if err != nil {
return errors.IsNotFound(err)
}
return false
}
}
// GatewayIsScheduled returns a function that checks if a Gateway is scheduled.
func GatewayIsScheduled(t *testing.T, ctx context.Context, gatewayNSN types.NamespacedName, clients K8sClients) func() bool {
return func() bool {
return gatewayutils.IsScheduled(MustGetGateway(t, ctx, gatewayNSN, clients))
}
}
// GatewayIsProgrammed returns a function that checks if a Gateway is programmed.
func GatewayIsProgrammed(t *testing.T, ctx context.Context, gatewayNSN types.NamespacedName, clients K8sClients) func() bool {
return func() bool {
return gatewayutils.IsProgrammed(MustGetGateway(t, ctx, gatewayNSN, clients))
}
}
// GatewayListenersAreProgrammed returns a function that checks if a Gateway's listeners are programmed.
func GatewayListenersAreProgrammed(t *testing.T, ctx context.Context, gatewayNSN types.NamespacedName, clients K8sClients) func() bool {
return func() bool {
return gatewayutils.AreListenersProgrammed(MustGetGateway(t, ctx, gatewayNSN, clients))
}
}
// GatewayDataPlaneIsReady returns a function that checks if a Gateway's DataPlane is ready.
func GatewayDataPlaneIsReady(t *testing.T, ctx context.Context, gateway *gwtypes.Gateway, clients K8sClients) func() bool {
return func() bool {
dataplanes := MustListDataPlanesForGateway(t, ctx, gateway, clients)
if len(dataplanes) == 1 {
// if the dataplane DeletionTimestamp is set, the dataplane deletion has been requested.
// Hence we cannot consider it as a valid dataplane that's ready.
if dataplanes[0].DeletionTimestamp != nil {
return false
}
for _, condition := range dataplanes[0].Status.Conditions {
if condition.Type == string(k8sutils.ReadyType) &&
condition.Status == metav1.ConditionTrue {
return true
}
}
}
return false
}
}
// GatewayControlPlaneIsProvisioned returns a function that checks if a Gateway's ControlPlane is provisioned.
func GatewayControlPlaneIsProvisioned(t *testing.T, ctx context.Context, gateway *gwtypes.Gateway, clients K8sClients) func() bool {
return func() bool {
controlPlanes := MustListControlPlanesForGateway(t, ctx, gateway, clients)
if len(controlPlanes) == 1 {
// if the controlplane DeletionTimestamp is set, the controlplane deletion has been requested.
// Hence we cannot consider it as a provisioned valid controlplane.
if controlPlanes[0].DeletionTimestamp != nil {
return false
}
for _, condition := range controlPlanes[0].Status.Conditions {
if condition.Type == string(controlplane.ConditionTypeProvisioned) &&
condition.Status == metav1.ConditionTrue {
return true
}
}
}
return false
}
}
// GatewayNetworkPoliciesExist is a helper function for tests that returns a function
// that can be used to check if a Gateway owns a networkpolicy.
// Should be used in conjunction with require.Eventually or assert.Eventually.
// Gateway object argument does need to exist in the cluster, thus, the function
// may be used with Not after the gateway has been deleted, to verify that
// the networkpolicy has been deleted too.
func GatewayNetworkPoliciesExist(t *testing.T, ctx context.Context, gateway *gwtypes.Gateway, clients K8sClients) func() bool {
return func() bool {
networkpolicies, err := gatewayutils.ListNetworkPoliciesForGateway(ctx, clients.MgrClient, gateway)
if err != nil {
return false
}
return len(networkpolicies) > 0
}
}
type ingressRuleT interface {
netv1.NetworkPolicyIngressRule | netv1.NetworkPolicyEgressRule
}
// GatewayNetworkPolicyForGatewayContainsRules is a helper function for tets that
// returns a function that can be used to check if exactly 1 NetworkPolicy exist
// for Gateway and if it contains all the provided rules.
func GatewayNetworkPolicyForGatewayContainsRules[T ingressRuleT](t *testing.T, ctx context.Context, gateway *gwtypes.Gateway, clients K8sClients, rules ...T) func() bool {
return func() bool {
networkpolicies, err := gatewayutils.ListNetworkPoliciesForGateway(ctx, clients.MgrClient, gateway)
if err != nil {
return false
}
if len(networkpolicies) != 1 {
return false
}
netpol := networkpolicies[0]
for _, rule := range rules {
switch r := any(rule).(type) {
case netv1.NetworkPolicyIngressRule:
if !networkPolicyRuleSliceContainsRule(netpol.Spec.Ingress, r) {
return false
}
case netv1.NetworkPolicyEgressRule:
if !networkPolicyRuleSliceContainsRule(netpol.Spec.Egress, r) {
return false
}
default:
t.Logf("NetworkPolicy rule has an unknown type %T", rule)
}
}
return true
}
}
func networkPolicyRuleSliceContainsRule[T ingressRuleT](rules []T, rule T) bool {
for _, r := range rules {
if cmp.Equal(r, rule) {
return true
}
}
return false
}
// GatewayIPAddressExist checks if a Gateway has IP addresses.
func GatewayIPAddressExist(t *testing.T, ctx context.Context, gatewayNSN types.NamespacedName, clients K8sClients) func() bool {
return func() bool {
gateway := MustGetGateway(t, ctx, gatewayNSN, clients)
if len(gateway.Status.Addresses) > 0 && *gateway.Status.Addresses[0].Type == gatewayv1.IPAddressType {
return true
}
return false
}
}
// GetResponseBodyContains issues an HTTP request and checks if a response body contains a string.
func GetResponseBodyContains(t *testing.T, ctx context.Context, clients K8sClients, httpc http.Client, url string, method string, responseContains string) func() bool {
return func() bool {
req, err := http.NewRequestWithContext(ctx, method, url, nil)
require.NoError(t, err)
resp, err := httpc.Do(req)
if err != nil {
return false
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
return strings.Contains(string(body), responseContains)
}
}
// Not is a helper function for tests that returns a negation of a predicate.
func Not(predicate func() bool) func() bool {
return func() bool {
return !predicate()
}
}