@@ -142,9 +142,13 @@ limitations under the License.
142142package e2e
143143
144144import (
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
324521const utilsTemplate = `/*
@@ -340,6 +537,7 @@ limitations under the License.
340537package utils
341538
342539import (
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
469690const targetTemplate = `
0 commit comments