Skip to content

Commit d279035

Browse files
camilamacedo86mogsie
authored andcommitted
✨ add metrics check and further helpers for the e2e tests
Provide further improvements for e2e tests test to help users be aware of how to tests using the metrics endpoint and validate if the metrics are properly expose.
1 parent 33a2f3d commit d279035

File tree

24 files changed

+1817
-227
lines changed

24 files changed

+1817
-227
lines changed

docs/book/src/cronjob-tutorial/testdata/project/test/e2e/e2e_suite_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ func TestE2E(t *testing.T) {
5757
}
5858

5959
var _ = BeforeSuite(func() {
60+
By("Ensure that Prometheus is enabled")
61+
_ = utils.UncommentCode("config/default/kustomization.yaml", "#- ../prometheus", "#")
62+
6063
By("generating files")
6164
cmd := exec.Command("make", "generate")
6265
_, err := utils.Run(cmd)

docs/book/src/cronjob-tutorial/testdata/project/test/e2e/e2e_test.go

Lines changed: 173 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ limitations under the License.
1717
package e2e
1818

1919
import (
20+
"encoding/json"
2021
"fmt"
22+
"os"
2123
"os/exec"
24+
"path/filepath"
2225
"time"
2326

2427
. "github.com/onsi/ginkgo/v2"
@@ -27,34 +30,44 @@ import (
2730
"tutorial.kubebuilder.io/project/test/utils"
2831
)
2932

33+
// namespace where the project is deployed in
3034
const namespace = "project-system"
3135

32-
// Define a set of end-to-end (e2e) tests to validate the behavior of the controller.
33-
var _ = Describe("controller", Ordered, func() {
36+
// serviceAccountName created for the project
37+
const serviceAccountName = "project-controller-manager"
38+
39+
// metricsServiceName is the name of the metrics service of the project
40+
const metricsServiceName = "project-controller-manager-metrics-service"
41+
42+
// metricsRoleBindingName is the name of the RBAC that will be created to allow get the metrics data
43+
const metricsRoleBindingName = "project-metrics-binding"
44+
45+
var _ = Describe("Manager", Ordered, func() {
3446
// Before running the tests, set up the environment by creating the namespace,
3547
// installing CRDs, and deploying the controller.
3648
BeforeAll(func() {
3749
By("creating manager namespace")
3850
cmd := exec.Command("kubectl", "create", "ns", namespace)
39-
_, err := utils.Run(cmd)
40-
ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to create namespace")
51+
Expect(utils.Run(cmd)).Error().NotTo(HaveOccurred(), "Failed to create namespace")
4152

4253
By("installing CRDs")
4354
cmd = exec.Command("make", "install")
44-
_, err = utils.Run(cmd)
45-
ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to install CRDs")
55+
Expect(utils.Run(cmd)).Error().NotTo(HaveOccurred(), "Failed to install CRDs")
4656

4757
By("deploying the controller-manager")
4858
cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", projectImage))
49-
_, err = utils.Run(cmd)
50-
ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to deploy the controller-manager")
59+
Expect(utils.Run(cmd)).Error().NotTo(HaveOccurred(), "Failed to deploy the controller-manager")
5160
})
5261

5362
// After all tests have been executed, clean up by undeploying the controller, uninstalling CRDs,
5463
// and deleting the namespace.
5564
AfterAll(func() {
65+
By("cleaning up the curl pod for metrics")
66+
cmd := exec.Command("kubectl", "delete", "pod", "curl-metrics", "-n", namespace)
67+
_, _ = utils.Run(cmd)
68+
5669
By("undeploying the controller-manager")
57-
cmd := exec.Command("make", "undeploy")
70+
cmd = exec.Command("make", "undeploy")
5871
_, _ = utils.Run(cmd)
5972

6073
By("uninstalling CRDs")
@@ -66,13 +79,15 @@ var _ = Describe("controller", Ordered, func() {
6679
_, _ = utils.Run(cmd)
6780
})
6881

69-
// The Context block contains the actual tests that validate the operator's behavior.
70-
Context("Operator", func() {
71-
It("should run successfully", func() {
72-
var controllerPodName string
82+
SetDefaultEventuallyTimeout(2 * time.Minute)
83+
SetDefaultEventuallyPollingInterval(time.Second)
7384

85+
// The Context block contains the actual tests that validate the manager's behavior.
86+
Context("Manager", func() {
87+
var controllerPodName string
88+
It("should run successfully", func() {
7489
By("validating that the controller-manager pod is running as expected")
75-
verifyControllerUp := func() error {
90+
verifyControllerUp := func(g Gomega) {
7691
// Get the name of the controller-manager pod
7792
cmd := exec.Command("kubectl", "get",
7893
"pods", "-l", "control-plane=controller-manager",
@@ -84,31 +99,161 @@ var _ = Describe("controller", Ordered, func() {
8499
)
85100

86101
podOutput, err := utils.Run(cmd)
87-
ExpectWithOffset(2, err).NotTo(HaveOccurred(), "Failed to retrieve controller-manager pod information")
102+
g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve controller-manager pod information")
88103
podNames := utils.GetNonEmptyLines(string(podOutput))
89-
if len(podNames) != 1 {
90-
return fmt.Errorf("expected 1 controller pod running, but got %d", len(podNames))
91-
}
104+
g.Expect(podNames).To(HaveLen(1), "expected 1 controller pod running")
92105
controllerPodName = podNames[0]
93-
ExpectWithOffset(2, controllerPodName).Should(ContainSubstring("controller-manager"))
106+
g.Expect(controllerPodName).To(ContainSubstring("controller-manager"))
94107

95108
// Validate the pod's status
96109
cmd = exec.Command("kubectl", "get",
97110
"pods", controllerPodName, "-o", "jsonpath={.status.phase}",
98111
"-n", namespace,
99112
)
100-
status, err := utils.Run(cmd)
101-
ExpectWithOffset(2, err).NotTo(HaveOccurred(), "Failed to retrieve controller-manager pod status")
102-
if string(status) != "Running" {
103-
return fmt.Errorf("controller pod in %s status", status)
104-
}
105-
return nil
113+
g.Expect(utils.Run(cmd)).To(BeEquivalentTo("Running"), "Incorrect controller-manager pod status")
106114
}
107115
// Repeatedly check if the controller-manager pod is running until it succeeds or times out.
108-
EventuallyWithOffset(1, verifyControllerUp, time.Minute, time.Second).Should(Succeed())
116+
Eventually(verifyControllerUp).Should(Succeed())
117+
})
118+
119+
It("should ensure the metrics endpoint is serving metrics", func() {
120+
By("creating a ClusterRoleBinding for the service account to allow access to metrics")
121+
cmd := exec.Command("kubectl", "create", "clusterrolebinding", metricsRoleBindingName,
122+
"--clusterrole=project-metrics-reader",
123+
fmt.Sprintf("--serviceaccount=%s:%s", namespace, serviceAccountName),
124+
)
125+
_, err := utils.Run(cmd)
126+
Expect(err).NotTo(HaveOccurred(), "Failed to create ClusterRoleBinding")
127+
128+
By("validating that the metrics service is available")
129+
cmd = exec.Command("kubectl", "get", "service", metricsServiceName, "-n", namespace)
130+
_, err = utils.Run(cmd)
131+
Expect(err).NotTo(HaveOccurred(), "Metrics service should exist")
132+
133+
By("validating that the ServiceMonitor for Prometheus is applied in the namespace")
134+
cmd = exec.Command("kubectl", "get", "ServiceMonitor", "-n", namespace)
135+
_, err = utils.Run(cmd)
136+
Expect(err).NotTo(HaveOccurred(), "ServiceMonitor should exist")
137+
138+
By("getting the service account token")
139+
token, err := serviceAccountToken()
140+
Expect(err).NotTo(HaveOccurred())
141+
Expect(token).NotTo(BeEmpty())
142+
143+
By("waiting for the metrics endpoint to be ready")
144+
verifyMetricsEndpointReady := func(g Gomega) {
145+
cmd := exec.Command("kubectl", "get", "endpoints", metricsServiceName, "-n", namespace)
146+
output, err := utils.Run(cmd)
147+
g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve endpoints information")
148+
g.Expect(string(output)).To(ContainSubstring("8443"), "Metrics endpoint is not ready")
149+
}
150+
Eventually(verifyMetricsEndpointReady).Should(Succeed())
151+
152+
By("verifying that the controller manager is serving the metrics server")
153+
verifyMetricsServerStarted := func(g Gomega) {
154+
cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace)
155+
logs, err := utils.Run(cmd)
156+
g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve controller manager logs")
157+
g.Expect(string(logs)).To(ContainSubstring("controller-runtime.metrics\tServing metrics server"),
158+
"Metrics server not yet started")
159+
}
160+
Eventually(verifyMetricsServerStarted).Should(Succeed())
161+
162+
By("creating the curl-metrics pod to access the metrics endpoint")
163+
cmd = exec.Command("kubectl", "run", "curl-metrics", "--restart=Never",
164+
"--namespace", namespace,
165+
"--image=curlimages/curl:7.78.0",
166+
"--", "/bin/sh", "-c", fmt.Sprintf(
167+
"curl -v -k -H 'Authorization: Bearer %s' https://%s.%s.svc.cluster.local:8443/metrics",
168+
token, metricsServiceName, namespace))
169+
Expect(utils.Run(cmd)).Error().NotTo(HaveOccurred(), "Failed to create curl-metrics pod")
170+
171+
By("waiting for the curl-metrics pod to complete.")
172+
verifyCurlUp := func(g Gomega) {
173+
cmd := exec.Command("kubectl", "get", "pods", "curl-metrics",
174+
"-o", "jsonpath={.status.phase}",
175+
"-n", namespace)
176+
g.Expect(utils.Run(cmd)).To(BeEquivalentTo("Succeeded"), "curl pod in wrong status")
177+
}
178+
Eventually(verifyCurlUp, 5*time.Minute).Should(Succeed())
179+
180+
By("getting the metrics by checking curl-metrics logs")
181+
metricsOutput := getMetricsOutput()
182+
Expect(metricsOutput).To(ContainSubstring(
183+
"controller_runtime_reconcile_total",
184+
))
109185
})
110186

111-
// TODO(user): Customize the e2e test suite to include
112-
// additional scenarios specific to your project.
187+
// TODO: Customize the e2e test suite with scenarios specific to your project.
188+
// Consider applying sample/CR(s) and check their status and/or verifying
189+
// the reconciliation by using the metrics, i.e.:
190+
// metricsOutput := getMetricsOutput()
191+
// Expect(metricsOutput).To(ContainSubstring(
192+
// fmt.Sprintf(`controller_runtime_reconcile_total{controller="%s",result="success"} 1`,
193+
// strings.ToLower(<Kind>),
194+
// ))
113195
})
114196
})
197+
198+
// serviceAccountToken returns a token for the specified service account in the given namespace.
199+
// It uses the Kubernetes TokenRequest API to generate a token by directly sending a request
200+
// and parsing the resulting token from the API response.
201+
func serviceAccountToken() (string, error) {
202+
const tokenRequestRawString = `{
203+
"apiVersion": "authentication.k8s.io/v1",
204+
"kind": "TokenRequest"
205+
}`
206+
207+
// Temporary file to store the token request
208+
secretName := fmt.Sprintf("%s-token-request", serviceAccountName)
209+
tokenRequestFile := filepath.Join("/tmp", secretName)
210+
err := os.WriteFile(tokenRequestFile, []byte(tokenRequestRawString), os.FileMode(0o644))
211+
if err != nil {
212+
return "", err
213+
}
214+
215+
var out string
216+
var rawJson string
217+
verifyTokenCreation := func(g Gomega) {
218+
// Execute kubectl command to create the token
219+
cmd := exec.Command("kubectl", "create", "--raw", fmt.Sprintf(
220+
"/api/v1/namespaces/%s/serviceaccounts/%s/token",
221+
namespace,
222+
serviceAccountName,
223+
), "-f", tokenRequestFile)
224+
225+
output, err := cmd.CombinedOutput()
226+
g.Expect(err).NotTo(HaveOccurred())
227+
228+
rawJson = string(output)
229+
230+
// Parse the JSON output to extract the token
231+
var token tokenRequest
232+
err = json.Unmarshal([]byte(rawJson), &token)
233+
g.Expect(err).NotTo(HaveOccurred())
234+
235+
out = token.Status.Token
236+
}
237+
Eventually(verifyTokenCreation).Should(Succeed())
238+
239+
return out, err
240+
}
241+
242+
// getMetricsOutput retrieves and returns the logs from the curl pod used to access the metrics endpoint.
243+
func getMetricsOutput() string {
244+
By("getting the curl-metrics logs")
245+
cmd := exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace)
246+
metricsOutput, err := utils.Run(cmd)
247+
Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod")
248+
metricsOutputStr := string(metricsOutput)
249+
Expect(metricsOutputStr).To(ContainSubstring("< HTTP/1.1 200 OK"))
250+
return metricsOutputStr
251+
}
252+
253+
// tokenRequest is a simplified representation of the Kubernetes TokenRequest API response,
254+
// containing only the token field that we need to extract.
255+
type tokenRequest struct {
256+
Status struct {
257+
Token string `json:"token"`
258+
} `json:"status"`
259+
}

docs/book/src/cronjob-tutorial/testdata/project/test/utils/utils.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ limitations under the License.
1717
package utils
1818

1919
import (
20+
"bufio"
21+
"bytes"
2022
"fmt"
2123
"os"
2224
"os/exec"
@@ -198,3 +200,52 @@ func GetProjectDir() (string, error) {
198200
wd = strings.Replace(wd, "/test/e2e", "", -1)
199201
return wd, nil
200202
}
203+
204+
// UncommentCode searches for target in the file and remove the comment prefix
205+
// of the target content. The target content may span multiple lines.
206+
func UncommentCode(filename, target, prefix string) error {
207+
// false positive
208+
// nolint:gosec
209+
content, err := os.ReadFile(filename)
210+
if err != nil {
211+
return err
212+
}
213+
strContent := string(content)
214+
215+
idx := strings.Index(strContent, target)
216+
if idx < 0 {
217+
return fmt.Errorf("unable to find the code %s to be uncomment", target)
218+
}
219+
220+
out := new(bytes.Buffer)
221+
_, err = out.Write(content[:idx])
222+
if err != nil {
223+
return err
224+
}
225+
226+
scanner := bufio.NewScanner(bytes.NewBufferString(target))
227+
if !scanner.Scan() {
228+
return nil
229+
}
230+
for {
231+
_, err := out.WriteString(strings.TrimPrefix(scanner.Text(), prefix))
232+
if err != nil {
233+
return err
234+
}
235+
// Avoid writing a newline in case the previous line was the last in target.
236+
if !scanner.Scan() {
237+
break
238+
}
239+
if _, err := out.WriteString("\n"); err != nil {
240+
return err
241+
}
242+
}
243+
244+
_, err = out.Write(content[idx+len(target):])
245+
if err != nil {
246+
return err
247+
}
248+
// false positive
249+
// nolint:gosec
250+
return os.WriteFile(filename, out.Bytes(), 0644)
251+
}

docs/book/src/getting-started/testdata/project/test/e2e/e2e_suite_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ func TestE2E(t *testing.T) {
5757
}
5858

5959
var _ = BeforeSuite(func() {
60+
By("Ensure that Prometheus is enabled")
61+
_ = utils.UncommentCode("config/default/kustomization.yaml", "#- ../prometheus", "#")
62+
6063
By("generating files")
6164
cmd := exec.Command("make", "generate")
6265
_, err := utils.Run(cmd)

0 commit comments

Comments
 (0)