@@ -142,9 +142,13 @@ limitations under the License.
142
142
package e2e
143
143
144
144
import (
145
+ "bufio"
146
+ "encoding/json"
145
147
"fmt"
148
+ "os"
146
149
"os/exec"
147
150
"path/filepath"
151
+ "strconv"
148
152
"strings"
149
153
"time"
150
154
@@ -159,77 +163,105 @@ import (
159
163
"github.com/example/memcached-operator/test/utils"
160
164
)
161
165
162
- // namespace store the ns where the Operator and Operand will be executed
163
- const namespace = "memcached-operator-system"
164
-
165
- var _ = Describe("memcached", func() {
166
-
167
- Context("ensure that Operator and Operand(s) can run in restricted namespaces", func() {
168
- BeforeEach(func() {
169
- // The prometheus and the certmanager are installed in this test
170
- // because the Memcached sample has this option enable and
171
- // when we try to apply the manifests both will be required to be installed
172
- By("installing prometheus operator")
173
- Expect(utils.InstallPrometheusOperator()).To(Succeed())
174
-
175
- By("installing the cert-manager")
176
- Expect(utils.InstallCertManager()).To(Succeed())
177
-
178
- // The namespace can be created when we run make install
179
- // However, in this test we want to ensure that the solution
180
- // can run in a ns labeled as restricted. Therefore, we are
181
- // creating the namespace and labeling it.
182
- By("creating manager namespace")
183
- cmd := exec.Command("kubectl", "create", "ns", namespace)
184
- _, _ = utils.Run(cmd)
185
-
186
- // Now, let's ensure that all namespaces can raise a Warn when we apply the manifests
187
- // and that the namespace where the Operator and Operand will run are enforced as
188
- // restricted so that we can ensure that both can be admitted and run with the enforcement
189
- By("labeling all namespaces to warn when we apply the manifest if it would violate the PodStandards")
190
- cmd = exec.Command("kubectl", "label", "--overwrite", "ns", "--all",
191
- "pod-security.kubernetes.io/audit=restricted",
192
- "pod-security.kubernetes.io/enforce-version=v1.24",
193
- "pod-security.kubernetes.io/warn=restricted")
194
- _, err := utils.Run(cmd)
195
- ExpectWithOffset(1, err).NotTo(HaveOccurred())
166
+ // constant parts of the file
167
+ const (
168
+ namespace = "memcached-operator-system"
169
+ memcachedDeploymentSizeUndesiredCountTotalName = "memcached_deployment_size_undesired_count_total"
170
+ tokenRequestRawString = "{\"apiVersion\": \"authentication.k8s.io/v1\", \"kind\": \"TokenRequest\"}"
171
+ )
196
172
197
- By("labeling enforce the namespace where the Operator and Operand(s) will run")
198
- cmd = exec.Command("kubectl", "label", "--overwrite", "ns", namespace,
199
- "pod-security.kubernetes.io/audit=restricted",
200
- "pod-security.kubernetes.io/enforce-version=v1.24",
201
- "pod-security.kubernetes.io/enforce=restricted")
202
- _, err = utils.Run(cmd)
203
- Expect(err).To(Not(HaveOccurred()))
204
- })
173
+ // tokenRequest is a trimmed down version of the authentication.k8s.io/v1/TokenRequest Type
174
+ // that we want to use for extracting the token.
175
+ type tokenRequest struct {
176
+ Status struct {
177
+ Token string "json:\"token\""
178
+ } "json:\"status\""
179
+ }
205
180
206
- AfterEach(func() {
207
- By("uninstalling the Prometheus manager bundle")
208
- utils.UninstallPrometheusOperator()
181
+ var _ = Describe("memcached", Ordered, func() {
182
+ BeforeAll(func() {
183
+ // The prometheus and the certmanager are installed in this test
184
+ // because the Memcached sample has this option enable and
185
+ // when we try to apply the manifests both will be required to be installed
186
+ By("installing prometheus operator")
187
+ Expect(utils.InstallPrometheusOperator()).To(Succeed())
188
+
189
+ By("installing the cert-manager")
190
+ Expect(utils.InstallCertManager()).To(Succeed())
191
+
192
+ // The namespace can be created when we run make install
193
+ // However, in this test we want ensure that the solution
194
+ // can run in a ns labeled as restricted. Therefore, we are
195
+ // creating the namespace an lebeling it.
196
+ By("creating manager namespace")
197
+ cmd := exec.Command("kubectl", "create", "ns", namespace)
198
+ _, _ = utils.Run(cmd)
199
+
200
+ // Now, let's ensure that all namespaces can raise an Warn when we apply the manifests
201
+ // and that the namespace where the Operator and Operand will run are enforced as
202
+ // restricted so that we can ensure that both can be admitted and run with the enforcement
203
+ By("labeling all namespaces to warn when we apply the manifest if would violate the PodStandards")
204
+ cmd = exec.Command("kubectl", "label", "--overwrite", "ns", "--all",
205
+ "pod-security.kubernetes.io/audit=restricted",
206
+ "pod-security.kubernetes.io/enforce-version=v1.24",
207
+ "pod-security.kubernetes.io/warn=restricted")
208
+ _, err := utils.Run(cmd)
209
+ ExpectWithOffset(1, err).NotTo(HaveOccurred())
210
+
211
+ By("labeling enforce the namespace where the Operator and Operand(s) will run")
212
+ cmd = exec.Command("kubectl", "label", "--overwrite", "ns", namespace,
213
+ "pod-security.kubernetes.io/audit=restricted",
214
+ "pod-security.kubernetes.io/enforce-version=v1.24",
215
+ "pod-security.kubernetes.io/enforce=restricted")
216
+ _, err = utils.Run(cmd)
217
+ Expect(err).To(Not(HaveOccurred()))
218
+
219
+ By("uncommenting all sections with 'monitoring' to enable operator custom metrics")
220
+ err = utils.ReplaceInFile("controllers/memcached_controller.go",
221
+ "//"+monitoringImportFragment, monitoringImportFragment)
222
+ Expect(err).To(Not(HaveOccurred()))
223
+
224
+ err = utils.ReplaceInFile("controllers/memcached_controller.go",
225
+ "//"+incMemcachedDeploymentSizeUndesiredCountTotalFragment, incMemcachedDeploymentSizeUndesiredCountTotalFragment)
226
+ Expect(err).To(Not(HaveOccurred()))
227
+
228
+ err = utils.ReplaceInFile("main.go",
229
+ "//"+monitoringImportFragment, monitoringImportFragment)
230
+ Expect(err).To(Not(HaveOccurred()))
231
+
232
+ err = utils.ReplaceInFile("main.go",
233
+ "//"+registerMetricsFragment, registerMetricsFragment)
234
+ Expect(err).To(Not(HaveOccurred()))
235
+ })
209
236
210
- By("uninstalling the cert-manager bundle")
211
- utils.UninstallCertManager()
237
+ AfterAll(func() {
238
+ By("uninstalling the Prometheus manager bundle")
239
+ utils.UninstallPrometheusOperator()
212
240
213
- By("removing manager namespace")
214
- cmd := exec.Command("kubectl", "create", "ns", namespace)
215
- _, _ = utils.Run(cmd)
216
- })
241
+ By("uninstalling the cert-manager bundle")
242
+ utils.UninstallCertManager()
243
+
244
+ By("removing manager namespace")
245
+ cmd := exec.Command("kubectl", "create", "ns", namespace)
246
+ _, _ = utils.Run(cmd)
247
+ })
217
248
218
- It("should successfully run the Memcached Operator", func() {
249
+ Context("Memcached Operator", func() {
250
+ It("should run successfully", func() {
219
251
var controllerPodName string
220
252
var err error
221
253
projectDir, _ := utils.GetProjectDir()
222
254
223
- // operatorImage store the name of the imahe used in the example
224
- const operatorImage = "example.com/memcached-operator:v0.0.1"
255
+ // operatorImage stores the name of the image used in the example
256
+ var operatorImage = "example.com/memcached-operator:v0.0.1"
225
257
226
258
By("building the manager(Operator) image")
227
- cmd := exec.Command("make", "docker-build", "IMG=example.com/memcached-operator:v0.0.1" )
259
+ cmd := exec.Command("make", "docker-build", fmt.Sprintf( "IMG=%s", operatorImage) )
228
260
_, err = utils.Run(cmd)
229
261
ExpectWithOffset(1, err).NotTo(HaveOccurred())
230
262
231
263
By("loading the the manager(Operator) image on Kind")
232
- err = utils.LoadImageToKindClusterWithName("example.com/memcached-operator:v0.0.1" )
264
+ err = utils.LoadImageToKindClusterWithName(operatorImage )
233
265
ExpectWithOffset(1, err).NotTo(HaveOccurred())
234
266
235
267
By("installing CRDs")
@@ -287,7 +319,7 @@ var _ = Describe("memcached", func() {
287
319
288
320
By("validating that pod(s) status.phase=Running")
289
321
getMemcachedPodStatus := func() error {
290
- cmd = exec.Command("kubectl", "get",
322
+ cmd = exec.Command("kubectl", "get",
291
323
"pods", "-l", "app.kubernetes.io/name=Memcached",
292
324
"-o", "jsonpath={.items[*].status}", "-n", namespace,
293
325
)
@@ -318,7 +350,172 @@ var _ = Describe("memcached", func() {
318
350
Eventually(getStatus, time.Minute, time.Second).Should(Succeed())
319
351
})
320
352
})
353
+
354
+ Context("Memcached Operator metrics", Ordered, func() {
355
+ BeforeAll(func() {
356
+ By("granting permissions to access the metrics")
357
+ cmd := exec.Command("kubectl",
358
+ "create", "clusterrolebinding", "metrics-memcached-operator",
359
+ "--clusterrole=memcached-operator-metrics-reader",
360
+ fmt.Sprintf("--serviceaccount=%s:memcached-operator-controller-manager", namespace))
361
+ _, err := utils.Run(cmd)
362
+ Expect(err).NotTo(HaveOccurred())
363
+ })
364
+
365
+ AfterAll(func() {
366
+ By("removing permissions to access the metrics")
367
+ cmd := exec.Command("kubectl", "delete",
368
+ "clusterrolebinding", "metrics-memcached-operator")
369
+ _, err := utils.Run(cmd)
370
+ Expect(err).NotTo(HaveOccurred())
371
+ })
372
+
373
+ It("MemcachedDeploymentSizeUndesiredCountTotal should be increased when scaling the Memcached deployment", func() {
374
+ initialMetricValue := getMetricValue(memcachedDeploymentSizeUndesiredCountTotalName)
375
+
376
+ numberOfScales := 5
377
+ By(fmt.Sprintf("scaling memcached-samle deployment %d times", numberOfScales))
378
+ scaleMemcachedSampleDeployment(numberOfScales)
379
+
380
+ By(fmt.Sprintf("validating MemcachedDeploymentSizeUndesiredCountTotal has increased by %d", numberOfScales))
381
+ finalMetricValue := getMetricValue(memcachedDeploymentSizeUndesiredCountTotalName)
382
+ Expect(finalMetricValue).To(Equal(initialMetricValue + numberOfScales))
383
+ })
384
+ })
321
385
})
386
+
387
+ // getMetricValue will reach the Memcached operator metrics endpoint, validate the metric and extract its value
388
+ func getMetricValue(metricName string) int {
389
+ // reach the metrics endpoint and validate the metric exists
390
+ metricsEndpoint := curlMetrics()
391
+ ExpectWithOffset(1, metricsEndpoint).Should(ContainSubstring(metricName))
392
+
393
+ // extract the metric value
394
+ metricValue, err := strconv.Atoi(parseMetricValue(metricsEndpoint, metricName))
395
+ ExpectWithOffset(1, err).NotTo(HaveOccurred())
396
+
397
+ return metricValue
398
+ }
399
+
400
+ // curlMetrics curl's the /metrics endpoint, returning all logs once a 200 status is returned.
401
+ func curlMetrics() string {
402
+ By("reading the metrics token")
403
+ // Filter token query by service account in case more than one exists in a namespace.
404
+ token, err := serviceAccountToken()
405
+ ExpectWithOffset(2, err).NotTo(HaveOccurred())
406
+ ExpectWithOffset(2, len(token)).To(BeNumerically(">", 0))
407
+
408
+ By("creating a curl pod")
409
+ cmd := exec.Command("kubectl", "run", "curl", "--image=curlimages/curl:7.68.0",
410
+ "--restart=OnFailure", "-n", "default", "--", "curl", "-v", "-k", "-H",
411
+ fmt.Sprintf("Authorization: Bearer %s", strings.TrimSpace(token)),
412
+ fmt.Sprintf("https://memcached-operator-controller-manager-metrics-service.%s.svc:8443/metrics", namespace))
413
+ _, err = utils.Run(cmd)
414
+ ExpectWithOffset(2, err).NotTo(HaveOccurred())
415
+
416
+ By("validating that the curl pod is running as expected")
417
+ verifyCurlUp := func() error {
418
+ // Validate pod status
419
+ cmd := exec.Command("kubectl", "get", "pods", "curl",
420
+ "-o", "jsonpath={.status.phase}", "-n", "default")
421
+ statusOutput, err := utils.Run(cmd)
422
+ status := string(statusOutput)
423
+ ExpectWithOffset(3, err).NotTo(HaveOccurred())
424
+ if status != "Completed" && status != "Succeeded" {
425
+ return fmt.Errorf("curl pod in %s status", status)
426
+ }
427
+ return nil
428
+ }
429
+ EventuallyWithOffset(2, verifyCurlUp, 240*time.Second, time.Second).Should(Succeed())
430
+
431
+ By("validating that the metrics endpoint is serving as expected")
432
+ var metricsEndpoint string
433
+ getCurlLogs := func() string {
434
+ cmd = exec.Command("kubectl", "logs", "curl", "-n", "default")
435
+ metricsEndpointOutput, err := utils.Run(cmd)
436
+ ExpectWithOffset(3, err).NotTo(HaveOccurred())
437
+ metricsEndpoint = string(metricsEndpointOutput)
438
+ return metricsEndpoint
439
+ }
440
+ EventuallyWithOffset(2, getCurlLogs, 10*time.Second, time.Second).Should(ContainSubstring("< HTTP/2 200"))
441
+
442
+ By("cleaning up the curl pod")
443
+ cmd = exec.Command("kubectl", "delete",
444
+ "pods/curl", "-n", "default")
445
+ _, err = utils.Run(cmd)
446
+ ExpectWithOffset(3, err).NotTo(HaveOccurred())
447
+
448
+ return metricsEndpoint
449
+ }
450
+
451
+ // serviceAccountToken provides a helper function that can provide you with a service account
452
+ // token that you can use to interact with the service. This function leverages the k8s'
453
+ // TokenRequest API in raw format in order to make it generic for all version of the k8s that
454
+ // is currently being supported in kubebuilder test infra.
455
+ // TokenRequest API returns the token in raw JWT format itself. There is no conversion required.
456
+ func serviceAccountToken() (out string, err error) {
457
+ By("Creating the ServiceAccount token")
458
+ secretName := "memcached-operator-controller-manager-token-request"
459
+ projectDir, _ := utils.GetProjectDir()
460
+ tokenRequestFile := filepath.Join(projectDir, "/test/e2e/", secretName)
461
+ err = os.WriteFile(tokenRequestFile, []byte(tokenRequestRawString), os.FileMode(0o755))
462
+ if err != nil {
463
+ return out, err
464
+ }
465
+ var rawJson string
466
+ Eventually(func() error {
467
+ // Output of this is already a valid JWT token. No need to covert this from base64 to string format
468
+ cmd := exec.Command("kubectl", "create", "--raw",
469
+ fmt.Sprintf("/api/v1/namespaces/%s/serviceaccounts/memcached-operator-controller-manager/token", namespace),
470
+ "-f", tokenRequestFile,
471
+ )
472
+ rawJsonOutput, err := utils.Run(cmd)
473
+ rawJson = string(rawJsonOutput)
474
+ if err != nil {
475
+ return err
476
+ }
477
+ var token tokenRequest
478
+ err = json.Unmarshal([]byte(rawJson), &token)
479
+ if err != nil {
480
+ return err
481
+ }
482
+ out = token.Status.Token
483
+ return nil
484
+ }, time.Minute, time.Second).Should(Succeed())
485
+
486
+ return out, err
487
+ }
488
+
489
+ // parseMetricValue will parse the metric value from the metrics endpoint
490
+ func parseMetricValue(metricsEndpoint string, metricName string) string {
491
+ r := strings.NewReader(metricsEndpoint)
492
+ scan := bufio.NewScanner(r)
493
+ for scan.Scan() {
494
+ metricLine := scan.Text()
495
+ if strings.HasPrefix(metricLine, metricName) {
496
+ split := strings.Split(metricLine, " ")
497
+ return split[1]
498
+ }
499
+ }
500
+ return ""
501
+ }
502
+
503
+ // scaleMemcachedSampleDeployment will scale memcached-sample deployment 'numberOfScales' times
504
+ func scaleMemcachedSampleDeployment(numberOfScales int) {
505
+ for i := 1; i <= numberOfScales; i++ {
506
+ cmd := exec.Command("kubectl", "scale", "--replicas=3",
507
+ "deployment", "memcached-sample", "-n", namespace)
508
+ _, err := utils.Run(cmd)
509
+ ExpectWithOffset(1, err).NotTo(HaveOccurred())
510
+ time.Sleep(10 * time.Second)
511
+ }
512
+ }
513
+
514
+ const monitoringImportFragment = "\"github.com/example/memcached-operator/monitoring\""
515
+
516
+ const incMemcachedDeploymentSizeUndesiredCountTotalFragment = "monitoring.MemcachedDeploymentSizeUndesiredCountTotal.Inc()"
517
+
518
+ const registerMetricsFragment = "monitoring.RegisterMetrics()"
322
519
`
323
520
324
521
const utilsTemplate = `/*
@@ -340,6 +537,7 @@ limitations under the License.
340
537
package utils
341
538
342
539
import (
540
+ "errors"
343
541
"fmt"
344
542
"os"
345
543
"os/exec"
@@ -464,6 +662,29 @@ func GetProjectDir() (string, error) {
464
662
wd = strings.Replace(wd, "/test/e2e", "", -1)
465
663
return wd, nil
466
664
}
665
+
666
+ // ReplaceInFile replaces all instances of old with new in the file at path.
667
+ func ReplaceInFile(path, old, new string) error {
668
+ info, err := os.Stat(path)
669
+ if err != nil {
670
+ return err
671
+ }
672
+ // false positive
673
+ // nolint:gosec
674
+ b, err := os.ReadFile(path)
675
+ if err != nil {
676
+ return err
677
+ }
678
+ if !strings.Contains(string(b), old) {
679
+ return errors.New("unable to find the content to be replaced")
680
+ }
681
+ s := strings.Replace(string(b), old, new, -1)
682
+ err = os.WriteFile(path, []byte(s), info.Mode())
683
+ if err != nil {
684
+ return err
685
+ }
686
+ return nil
687
+ }
467
688
`
468
689
469
690
const targetTemplate = `
0 commit comments