Skip to content

Commit 0f81c62

Browse files
authored
Test for operator custom metrics (#6025)
1 parent 23bba25 commit 0f81c62

File tree

5 files changed

+832
-169
lines changed

5 files changed

+832
-169
lines changed

hack/generate/samples/internal/go/memcached-with-webhooks/e2e_test_code.go

Lines changed: 278 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,13 @@ limitations under the License.
142142
package e2e
143143
144144
import (
145+
"bufio"
146+
"encoding/json"
145147
"fmt"
148+
"os"
146149
"os/exec"
147150
"path/filepath"
151+
"strconv"
148152
"strings"
149153
"time"
150154
@@ -159,77 +163,105 @@ import (
159163
"github.com/example/memcached-operator/test/utils"
160164
)
161165
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+
)
196172
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+
}
205180
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+
})
209236
210-
By("uninstalling the cert-manager bundle")
211-
utils.UninstallCertManager()
237+
AfterAll(func() {
238+
By("uninstalling the Prometheus manager bundle")
239+
utils.UninstallPrometheusOperator()
212240
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+
})
217248
218-
It("should successfully run the Memcached Operator", func() {
249+
Context("Memcached Operator", func() {
250+
It("should run successfully", func() {
219251
var controllerPodName string
220252
var err error
221253
projectDir, _ := utils.GetProjectDir()
222254
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"
225257
226258
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))
228260
_, err = utils.Run(cmd)
229261
ExpectWithOffset(1, err).NotTo(HaveOccurred())
230262
231263
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)
233265
ExpectWithOffset(1, err).NotTo(HaveOccurred())
234266
235267
By("installing CRDs")
@@ -287,7 +319,7 @@ var _ = Describe("memcached", func() {
287319
288320
By("validating that pod(s) status.phase=Running")
289321
getMemcachedPodStatus := func() error {
290-
cmd = exec.Command("kubectl", "get",
322+
cmd = exec.Command("kubectl", "get",
291323
"pods", "-l", "app.kubernetes.io/name=Memcached",
292324
"-o", "jsonpath={.items[*].status}", "-n", namespace,
293325
)
@@ -318,7 +350,172 @@ var _ = Describe("memcached", func() {
318350
Eventually(getStatus, time.Minute, time.Second).Should(Succeed())
319351
})
320352
})
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+
})
321385
})
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()"
322519
`
323520

324521
const utilsTemplate = `/*
@@ -340,6 +537,7 @@ limitations under the License.
340537
package utils
341538
342539
import (
540+
"errors"
343541
"fmt"
344542
"os"
345543
"os/exec"
@@ -464,6 +662,29 @@ func GetProjectDir() (string, error) {
464662
wd = strings.Replace(wd, "/test/e2e", "", -1)
465663
return wd, nil
466664
}
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+
}
467688
`
468689

469690
const targetTemplate = `

0 commit comments

Comments
 (0)