@@ -6,14 +6,20 @@ import (
6
6
"fmt"
7
7
"os"
8
8
"path/filepath"
9
+ "regexp"
10
+ "strconv"
9
11
"strings"
10
12
"time"
11
13
12
14
g "github.com/onsi/ginkgo/v2"
13
15
o "github.com/onsi/gomega"
16
+ batchv1 "k8s.io/api/batch/v1"
17
+ corev1 "k8s.io/api/core/v1"
14
18
"k8s.io/apimachinery/pkg/api/meta"
15
19
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
20
+ "k8s.io/apimachinery/pkg/util/rand"
16
21
"k8s.io/apimachinery/pkg/util/wait"
22
+ "k8s.io/utils/ptr"
17
23
18
24
exutil "github.com/openshift/origin/test/extended/util"
19
25
)
@@ -71,7 +77,7 @@ var _ = g.Describe("[sig-olmv1][OCPFeatureGate:NewOLM] OLMv1 CRDs", func() {
71
77
})
72
78
})
73
79
74
- var _ = g .Describe ("[sig-olmv1][OCPFeatureGate:NewOLM][Skipped:Disconnected] OLMv1 Catalogs" , func () {
80
+ var _ = g .Describe ("[sig-olmv1][OCPFeatureGate:NewOLM][Skipped:Disconnected] OLMv1 default Catalogs" , func () {
75
81
defer g .GinkgoRecover ()
76
82
oc := exutil .NewCLIWithoutNamespace ("default" )
77
83
@@ -99,6 +105,244 @@ var _ = g.Describe("[sig-olmv1][OCPFeatureGate:NewOLM][Skipped:Disconnected] OLM
99
105
})
100
106
})
101
107
108
+ var _ = g .Describe ("[sig-olmv1][OCPFeatureGate:NewOLM][Skipped:Disconnected] OLMv1 Catalogs /v1/api/all endpoint" , func () {
109
+ defer g .GinkgoRecover ()
110
+ oc := exutil .NewCLIWithoutNamespace ("default" )
111
+
112
+ g .It ("should serve FBC" , func (ctx g.SpecContext ) {
113
+ checkFeatureCapability (ctx , oc )
114
+
115
+ g .By ("Testing /api/v1/all endpoint for catalog openshift-community-operators" )
116
+ verifyAPIEndpoint (ctx , oc , oc .Namespace (), "openshift-community-operators" , "all" )
117
+ })
118
+ })
119
+
120
+ var _ = g .Describe ("[sig-olmv1][OCPFeatureGate:NewOLMCatalogdAPIV1Metas][Skipped:Disconnected] OLMv1 Catalogs /v1/api/metas endpoint" , func () {
121
+ defer g .GinkgoRecover ()
122
+ oc := exutil .NewCLIWithoutNamespace ("default" )
123
+ g .It (" should serve the /v1/api/metas API endpoint" , func (ctx g.SpecContext ) {
124
+ checkFeatureCapability (ctx , oc )
125
+
126
+ g .By ("Testing api/v1/metas endpoint for catalog openshift-community-operators" )
127
+ verifyAPIEndpoint (ctx , oc , oc .Namespace (), "openshift-community-operators" , "metas?schema=olm.package" )
128
+ })
129
+ })
130
+
131
+ var _ = g .Describe ("[sig-olmv1][OCPFeatureGate:NewOLM][Skipped:Disconnected] OLMv1 Catalogs API Load Test /v1/api/all endpoint" , func () {
132
+ defer g .GinkgoRecover ()
133
+ oc := exutil .NewCLIWithoutNamespace ("default" )
134
+
135
+ g .It ("should handle concurrent load with acceptable performance" , func (ctx g.SpecContext ) {
136
+ checkFeatureCapability (ctx , oc )
137
+
138
+ // Parameters for the load test
139
+ concurrentConnections := 10 // Number of connections to keep open
140
+ testDurationSeconds := 30 // Duration of the test in seconds
141
+ requestRate := 100 // Requests per second (constant throughput)
142
+ maxLatencyThresholdMs := 20000 // Maximum acceptable P99 latency in milliseconds
143
+ successRateThreshold := 100.0 // Required success rate percentage
144
+
145
+ g .By ("Load testing /api/v1/%s endpoint for catalog openshift-community-operators" )
146
+
147
+ // Get the base URL for the catalog
148
+ baseURL , err := oc .AsAdmin ().Run ("get" ).Args (
149
+ "clustercatalogs.olm.operatorframework.io" ,
150
+ "openshift-community-operators" ,
151
+ "-o=jsonpath={.status.urls.base}" ).Output ()
152
+ o .Expect (err ).NotTo (o .HaveOccurred ())
153
+ o .Expect (baseURL ).NotTo (o .BeEmpty (), "Base URL not found for catalog openshift-community-operators" )
154
+
155
+ // Construct the full URL to test
156
+ serviceURL := fmt .Sprintf ("%s/api/v1/all" , baseURL )
157
+ g .GinkgoLogr .Info (fmt .Sprintf ("Testing URL: %s" , serviceURL ))
158
+
159
+ // Create a Job to run the wrk2 load test
160
+ jobName := "wrk2-test-openshift-community-operators-all"
161
+
162
+ // Using https://github.com/giltene/wrk2/ to run the load test
163
+ // wrk2 is a modern HTTP benchmarking tool capable of generating significant load. Lua scripts are used to analyze the results.
164
+ // https://www.lua.org/docs.html
165
+
166
+ // Lua script for the load test
167
+ luaScript := `
168
+ -- This script only gets summary information and prints a simple report
169
+ -- No attempt to access the latency data directly
170
+
171
+ function done(summary, latency, requests)
172
+ -- Print basic stats
173
+ io.write("\n\n=== LOAD TEST RESULTS ===\n")
174
+ io.write(string.format("Total requests: %d\n", summary.requests))
175
+ io.write(string.format("Socket errors: connect %d, read %d, write %d, timeout %d\n",
176
+ summary.errors.connect, summary.errors.read, summary.errors.write, summary.errors.timeout))
177
+ io.write(string.format("HTTP errors: %d\n", summary.errors.status))
178
+ io.write(string.format("Request rate: %.2f requests/s\n", summary.requests / summary.duration * 1000000))
179
+
180
+ -- Calculate success rate
181
+ local total_errors = summary.errors.status + summary.errors.connect +
182
+ summary.errors.read + summary.errors.write + summary.errors.timeout
183
+ local success_count = summary.requests - total_errors
184
+ local success_rate = success_count / summary.requests * 100
185
+ io.write(string.format("Success rate: %.2f%%\n", success_rate))
186
+
187
+ -- Check if we met our success rate threshold
188
+ local success_threshold = tonumber(os.getenv("SUCCESS_RATE_THRESHOLD") or 100)
189
+
190
+ io.write("\nThreshold checks:\n")
191
+ io.write(string.format(" Success rate: %.2f%% (threshold: %.2f%%)\n",
192
+ success_rate, success_threshold))
193
+
194
+ -- Only check success rate criteria
195
+ if success_rate < success_threshold then
196
+ io.write(string.format("FAILED: Success rate (%.2f%%) below threshold (%.2f%%)\n",
197
+ success_rate, success_threshold))
198
+ os.exit(1)
199
+ end
200
+
201
+ io.write("\nSUCCESS: Success rate criteria met. Check latency in wrk2 output.\n")
202
+ end
203
+ `
204
+
205
+ // Shell script for the job
206
+ shellScript := fmt .Sprintf (`
207
+ set -ex
208
+
209
+ # Set test parameters
210
+ SERVICE_URL="%s"
211
+ CONCURRENT_CONNECTIONS=%d
212
+ TEST_DURATION_SECONDS=%d
213
+ REQUEST_RATE=%d
214
+ MAX_LATENCY_THRESHOLD_MS=%d
215
+ SUCCESS_RATE_THRESHOLD=%.1f
216
+
217
+ # Install required packages
218
+ dnf install -y git gcc openssl-devel zlib-devel make
219
+
220
+ # Clone and build wrk2
221
+ git clone https://github.com/giltene/wrk2.git
222
+ cd wrk2
223
+ make
224
+
225
+ # Create Lua script
226
+ cat > bare_minimum.lua << 'EOL'
227
+ %s
228
+ EOL
229
+
230
+ # Export environment variables for Lua script
231
+ export MAX_LATENCY_THRESHOLD_MS=$MAX_LATENCY_THRESHOLD_MS
232
+ export SUCCESS_RATE_THRESHOLD=$SUCCESS_RATE_THRESHOLD
233
+
234
+ # Run the load test
235
+ echo "Starting wrk2 load test..."
236
+ ./wrk -t$CONCURRENT_CONNECTIONS -c$CONCURRENT_CONNECTIONS -d${TEST_DURATION_SECONDS}s -R$REQUEST_RATE -s bare_minimum.lua -L $SERVICE_URL
237
+ ` , serviceURL , concurrentConnections , testDurationSeconds , requestRate , maxLatencyThresholdMs , successRateThreshold , luaScript )
238
+
239
+ job := & batchv1.Job {
240
+ ObjectMeta : metav1.ObjectMeta {
241
+ Name : jobName ,
242
+ Namespace : oc .Namespace (),
243
+ },
244
+ Spec : batchv1.JobSpec {
245
+ Template : corev1.PodTemplateSpec {
246
+ Spec : corev1.PodSpec {
247
+ Containers : []corev1.Container {
248
+ {
249
+ Name : "wrk2-tester" ,
250
+ Image : "registry.redhat.io/ubi8/ubi:latest" ,
251
+ Command : []string {
252
+ "/bin/bash" ,
253
+ "-c" ,
254
+ shellScript ,
255
+ },
256
+ },
257
+ },
258
+ RestartPolicy : corev1 .RestartPolicyNever ,
259
+ },
260
+ },
261
+ BackoffLimit : ptr .To (int32 (2 )),
262
+ },
263
+ }
264
+
265
+ // Create the job
266
+ _ , err = oc .AdminKubeClient ().BatchV1 ().Jobs (oc .Namespace ()).Create (context .TODO (), job , metav1.CreateOptions {})
267
+ o .Expect (err ).NotTo (o .HaveOccurred ())
268
+
269
+ // Wait for the job to complete
270
+ err = wait .PollUntilContextTimeout (ctx , 5 * time .Second , 10 * time .Minute , true , func (ctx context.Context ) (bool , error ) {
271
+ job , err := oc .AdminKubeClient ().BatchV1 ().Jobs (oc .Namespace ()).Get (context .TODO (), jobName , metav1.GetOptions {})
272
+ if err != nil {
273
+ return false , err
274
+ }
275
+
276
+ if job .Status .Succeeded > 0 {
277
+ return true , nil
278
+ }
279
+
280
+ if job .Status .Failed > 0 {
281
+ // Get logs to see why it failed
282
+ pods , err := oc .AdminKubeClient ().CoreV1 ().Pods (oc .Namespace ()).List (context .TODO (), metav1.ListOptions {
283
+ LabelSelector : fmt .Sprintf ("job-name=%s" , jobName ),
284
+ })
285
+ if err == nil && len (pods .Items ) > 0 {
286
+ logs , logErr := oc .AdminKubeClient ().CoreV1 ().Pods (oc .Namespace ()).GetLogs (pods .Items [0 ].Name , & corev1.PodLogOptions {}).DoRaw (context .TODO ())
287
+ if logErr == nil {
288
+ g .GinkgoLogr .Error (nil , fmt .Sprintf ("Load test job failed. Logs: %s" , string (logs )))
289
+ }
290
+ }
291
+ return false , fmt .Errorf ("load test job failed" )
292
+ }
293
+
294
+ return false , nil
295
+ })
296
+ o .Expect (err ).NotTo (o .HaveOccurred (), "Load test failed or timed out" )
297
+
298
+ // Get the logs from the job
299
+ pods , err := oc .AdminKubeClient ().CoreV1 ().Pods (oc .Namespace ()).List (context .TODO (), metav1.ListOptions {
300
+ LabelSelector : fmt .Sprintf ("job-name=%s" , jobName ),
301
+ })
302
+ o .Expect (err ).NotTo (o .HaveOccurred ())
303
+ o .Expect (pods .Items ).NotTo (o .BeEmpty ())
304
+
305
+ // Get and display the logs
306
+ logs , err := oc .AdminKubeClient ().CoreV1 ().Pods (oc .Namespace ()).GetLogs (pods .Items [0 ].Name , & corev1.PodLogOptions {}).DoRaw (context .TODO ())
307
+ o .Expect (err ).NotTo (o .HaveOccurred ())
308
+
309
+ logsStr := string (logs )
310
+ g .GinkgoLogr .Info (fmt .Sprintf ("Load test logs:\n %s" , logsStr ))
311
+
312
+ // Extract key metrics from the logs
313
+ successRateMatch := regexp .MustCompile (`Success rate: (\d+\.\d+)%` ).FindStringSubmatch (logsStr )
314
+ latencyMatch := regexp .MustCompile (`99.000%\s+(\d+\.\d+)` ).FindStringSubmatch (logsStr )
315
+ requestsPerSecMatch := regexp .MustCompile (`Requests/sec:\s+(\d+\.\d+)` ).FindStringSubmatch (logsStr )
316
+
317
+ // Verify metrics meet expectations
318
+ if len (successRateMatch ) >= 2 {
319
+ successRate , err := strconv .ParseFloat (successRateMatch [1 ], 64 )
320
+ o .Expect (err ).NotTo (o .HaveOccurred ())
321
+ o .Expect (successRate ).To (o .BeNumerically (">=" , successRateThreshold ),
322
+ fmt .Sprintf ("Success rate (%.2f%%) below threshold (%.2f%%)" , successRate , successRateThreshold ))
323
+ g .GinkgoLogr .Info (fmt .Sprintf ("Success rate: %.2f%% (threshold: %.2f%%)" , successRate , successRateThreshold ))
324
+ }
325
+
326
+ if len (latencyMatch ) >= 2 {
327
+ p99Latency , err := strconv .ParseFloat (latencyMatch [1 ], 64 )
328
+ o .Expect (err ).NotTo (o .HaveOccurred ())
329
+ // Convert from microseconds to milliseconds
330
+ p99LatencyMs := p99Latency / 1000.0
331
+ o .Expect (p99LatencyMs ).To (o .BeNumerically ("<=" , float64 (maxLatencyThresholdMs )),
332
+ fmt .Sprintf ("P99 latency (%.2f ms) exceeds threshold (%d ms)" , p99LatencyMs , maxLatencyThresholdMs ))
333
+ g .GinkgoLogr .Info (fmt .Sprintf ("P99 latency: %.2f ms (threshold: %d ms)" , p99LatencyMs , maxLatencyThresholdMs ))
334
+ }
335
+
336
+ if len (requestsPerSecMatch ) >= 2 {
337
+ rps , err := strconv .ParseFloat (requestsPerSecMatch [1 ], 64 )
338
+ o .Expect (err ).NotTo (o .HaveOccurred ())
339
+ g .GinkgoLogr .Info (fmt .Sprintf ("Requests per second: %.2f" , rps ))
340
+ }
341
+
342
+ g .By ("Successfully completed load test with acceptable performance" )
343
+ })
344
+ })
345
+
102
346
var _ = g .Describe ("[sig-olmv1][OCPFeatureGate:NewOLM][Skipped:Disconnected] OLMv1 New Catalog Install" , func () {
103
347
defer g .GinkgoRecover ()
104
348
@@ -419,3 +663,82 @@ func checkFeatureCapability(ctx context.Context, oc *exutil.CLI) {
419
663
g .Skip ("Test only runs with OperatorLifecycleManagerV1 capability" )
420
664
}
421
665
}
666
+
667
+ // verifyAPIEndpoint runs a job to validate the given service endpoint of a ClusterCatalog
668
+ func verifyAPIEndpoint (ctx g.SpecContext , oc * exutil.CLI , namespace , catalogName , endpoint string ) {
669
+ jobName := fmt .Sprintf ("test-catalog-%s-%s-%s" , catalogName , endpoint , rand .String (5 ))
670
+
671
+ baseURL , err := oc .AsAdmin ().Run ("get" ).Args (
672
+ "clustercatalogs.olm.operatorframework.io" ,
673
+ catalogName ,
674
+ "-o=jsonpath={.status.urls.base}" ).Output ()
675
+ o .Expect (err ).NotTo (o .HaveOccurred ())
676
+ o .Expect (baseURL ).NotTo (o .BeEmpty (), fmt .Sprintf ("Base URL not found for catalog %s" , catalogName ))
677
+
678
+ serviceURL := fmt .Sprintf ("%s/api/v1/%s" , baseURL , endpoint )
679
+ g .GinkgoLogr .Info (fmt .Sprintf ("Using service URL: %s" , serviceURL ))
680
+
681
+ job := & batchv1.Job {
682
+ ObjectMeta : metav1.ObjectMeta {
683
+ Name : jobName ,
684
+ Namespace : namespace ,
685
+ },
686
+ Spec : batchv1.JobSpec {
687
+ Template : corev1.PodTemplateSpec {
688
+ Spec : corev1.PodSpec {
689
+ Containers : []corev1.Container {
690
+ {
691
+ Name : "api-tester" ,
692
+ Image : "registry.redhat.io/rhel8/httpd-24:latest" ,
693
+ Command : []string {
694
+ "/bin/bash" ,
695
+ "-c" ,
696
+ fmt .Sprintf (`
697
+ set -ex
698
+ response=$(curl -s -k "%s" || echo "ERROR: Failed to access endpoint")
699
+ if [[ "$response" == ERROR* ]]; then
700
+ echo "$response"
701
+ exit 1
702
+ fi
703
+ echo "Successfully verified API endpoint"
704
+ exit 0
705
+ ` , serviceURL ),
706
+ },
707
+ },
708
+ },
709
+ RestartPolicy : corev1 .RestartPolicyNever ,
710
+ },
711
+ },
712
+ },
713
+ }
714
+
715
+ _ , err = oc .AdminKubeClient ().BatchV1 ().Jobs (namespace ).Create (context .TODO (), job , metav1.CreateOptions {})
716
+ o .Expect (err ).NotTo (o .HaveOccurred ())
717
+
718
+ err = wait .PollUntilContextTimeout (ctx , 5 * time .Second , 30 * time .Second , true , func (ctx context.Context ) (bool , error ) {
719
+ job , err := oc .AdminKubeClient ().BatchV1 ().Jobs (namespace ).Get (context .TODO (), jobName , metav1.GetOptions {})
720
+ if err != nil {
721
+ return false , err
722
+ }
723
+
724
+ if job .Status .Succeeded > 0 {
725
+ return true , nil
726
+ }
727
+ if job .Status .Failed > 0 {
728
+ return false , fmt .Errorf ("job failed" )
729
+ }
730
+
731
+ return false , nil
732
+ })
733
+ o .Expect (err ).NotTo (o .HaveOccurred ())
734
+
735
+ pods , err := oc .AdminKubeClient ().CoreV1 ().Pods (namespace ).List (context .TODO (), metav1.ListOptions {
736
+ LabelSelector : fmt .Sprintf ("job-name=%s" , jobName ),
737
+ })
738
+ o .Expect (err ).NotTo (o .HaveOccurred ())
739
+ o .Expect (pods .Items ).NotTo (o .BeEmpty ())
740
+
741
+ logs , err := oc .AdminKubeClient ().CoreV1 ().Pods (namespace ).GetLogs (pods .Items [0 ].Name , & corev1.PodLogOptions {}).DoRaw (context .TODO ())
742
+ o .Expect (err ).NotTo (o .HaveOccurred ())
743
+ g .GinkgoLogr .Info (fmt .Sprintf ("Job logs for %s endpoint: %s" , endpoint , string (logs )))
744
+ }
0 commit comments