Skip to content

Commit 94633cc

Browse files
✨ 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 163f4a2 commit 94633cc

File tree

23 files changed

+1434
-487
lines changed

23 files changed

+1434
-487
lines changed

.github/workflows/test-e2e-samples.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ jobs:
3838
run: |
3939
KUSTOMIZATION_FILE_PATH="testdata/project-v4/config/default/kustomization.yaml"
4040
sed -i '25s/^#//' $KUSTOMIZATION_FILE_PATH
41+
sed -i '27s/^#//' $KUSTOMIZATION_FILE_PATH
4142
sed -i '51s/^#//' $KUSTOMIZATION_FILE_PATH
4243
sed -i '55,151s/^#//' $KUSTOMIZATION_FILE_PATH
4344
cd testdata/project-v4/
@@ -58,6 +59,7 @@ jobs:
5859
run: |
5960
KUSTOMIZATION_FILE_PATH="testdata/project-v4-with-plugins/config/default/kustomization.yaml"
6061
sed -i '25s/^#//' $KUSTOMIZATION_FILE_PATH
62+
sed -i '27s/^#//' $KUSTOMIZATION_FILE_PATH
6163
sed -i '51s/^#//' $KUSTOMIZATION_FILE_PATH
6264
# Uncomment only ValidatingWebhookConfiguration
6365
# from cert-manager replaces

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 enable")
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: 180 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@ limitations under the License.
1717
package e2e
1818

1919
import (
20+
"encoding/json"
2021
"fmt"
22+
"os"
2123
"os/exec"
24+
"path/filepath"
25+
"strings"
2226
"time"
2327

2428
. "github.com/onsi/ginkgo/v2"
@@ -27,10 +31,19 @@ import (
2731
"tutorial.kubebuilder.io/project/test/utils"
2832
)
2933

34+
// namespace where the project is deployed in
3035
const namespace = "project-system"
3136

32-
// Define a set of end-to-end (e2e) tests to validate the behavior of the controller.
33-
var _ = Describe("controller", Ordered, func() {
37+
// serviceAccountName created for the project
38+
const serviceAccountName = "project-controller-manager"
39+
40+
// metricsServiceName is the name of the metrics service of the project
41+
const metricsServiceName = "project-controller-manager-metrics-service"
42+
43+
// metricsRoleBindingName is the name of the RBAC that will be created to allow get the metrics data
44+
const metricsRoleBindingName = "project-metrics-binding"
45+
46+
var _ = Describe("Manager", Ordered, func() {
3447
// Before running the tests, set up the environment by creating the namespace,
3548
// installing CRDs, and deploying the controller.
3649
BeforeAll(func() {
@@ -53,8 +66,12 @@ var _ = Describe("controller", Ordered, func() {
5366
// After all tests have been executed, clean up by undeploying the controller, uninstalling CRDs,
5467
// and deleting the namespace.
5568
AfterAll(func() {
69+
By("cleaning up the curl pod for metrics")
70+
cmd := exec.Command("kubectl", "delete", "pod", "curl-metrics", "-n", namespace)
71+
_, _ = utils.Run(cmd)
72+
5673
By("undeploying the controller-manager")
57-
cmd := exec.Command("make", "undeploy")
74+
cmd = exec.Command("make", "undeploy")
5875
_, _ = utils.Run(cmd)
5976

6077
By("uninstalling CRDs")
@@ -66,11 +83,10 @@ var _ = Describe("controller", Ordered, func() {
6683
_, _ = utils.Run(cmd)
6784
})
6885

69-
// The Context block contains the actual tests that validate the operator's behavior.
70-
Context("Operator", func() {
86+
// The Context block contains the actual tests that validate the manager's behavior.
87+
Context("Manager", func() {
88+
var controllerPodName string
7189
It("should run successfully", func() {
72-
var controllerPodName string
73-
7490
By("validating that the controller-manager pod is running as expected")
7591
verifyControllerUp := func() error {
7692
// Get the name of the controller-manager pod
@@ -149,7 +165,162 @@ var _ = Describe("controller", Ordered, func() {
149165

150166
// +kubebuilder:scaffold:e2e-webhooks-checks
151167

152-
// TODO(user): Customize the e2e test suite to include
153-
// additional scenarios specific to your project.
168+
It("should ensure the metrics endpoint is serving metrics", func() {
169+
By("creating a ClusterRoleBinding for the service account to allow access to metrics")
170+
cmd := exec.Command("kubectl", "create", "clusterrolebinding", metricsRoleBindingName,
171+
"--clusterrole=project-metrics-reader",
172+
fmt.Sprintf("--serviceaccount=%s:%s", namespace, serviceAccountName),
173+
)
174+
_, err := utils.Run(cmd)
175+
ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to create ClusterRoleBinding")
176+
177+
By("validating that the metrics service is available")
178+
cmd = exec.Command("kubectl", "get", "service", metricsServiceName, "-n", namespace)
179+
_, err = utils.Run(cmd)
180+
ExpectWithOffset(2, err).NotTo(HaveOccurred(), "Metrics service should exist")
181+
182+
By("validating that the ServiceMonitor for Prometheus is applied in the namespace")
183+
cmd = exec.Command("kubectl", "get", "ServiceMonitor", "-n", namespace)
184+
_, err = utils.Run(cmd)
185+
ExpectWithOffset(2, err).NotTo(HaveOccurred(), "ServiceMonitor should exist")
186+
187+
By("getting the service account token")
188+
token, err := serviceAccountToken()
189+
ExpectWithOffset(2, err).NotTo(HaveOccurred())
190+
ExpectWithOffset(2, token).NotTo(BeEmpty())
191+
192+
By("waiting for the metrics endpoint to be ready")
193+
verifyMetricsEndpointReady := func() error {
194+
cmd := exec.Command("kubectl", "get", "endpoints", metricsServiceName, "-n", namespace)
195+
output, err := utils.Run(cmd)
196+
if err != nil {
197+
return err
198+
}
199+
if !strings.Contains(string(output), "8443") {
200+
return fmt.Errorf("metrics endpoint is not ready")
201+
}
202+
return nil
203+
}
204+
EventuallyWithOffset(2, verifyMetricsEndpointReady, 2*time.Minute, 10*time.Second).Should(Succeed())
205+
206+
By("verifying that the controller manager is serving the metrics server")
207+
Eventually(func() error {
208+
cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace)
209+
logs, err := utils.Run(cmd)
210+
if err != nil {
211+
return err
212+
}
213+
if !strings.Contains(string(logs), "controller-runtime.metrics\tServing metrics server") {
214+
return fmt.Errorf("metrics server not yet started")
215+
}
216+
return nil
217+
}, 2*time.Minute, 10*time.Second).Should(Succeed(), "Controller manager did not start serving metrics server")
218+
219+
By("creating the curl-metrics pod to access the metrics endpoint")
220+
cmd = exec.Command("kubectl", "run", "curl-metrics", "--restart=Never",
221+
"--namespace", namespace,
222+
"--image=curlimages/curl:7.78.0",
223+
"--", "/bin/sh", "-c", fmt.Sprintf(
224+
"curl -v -k -H 'Authorization: Bearer %s' https://%s.%s.svc.cluster.local:8443/metrics",
225+
token, metricsServiceName, namespace))
226+
_, err = utils.Run(cmd)
227+
ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to create curl-metrics pod")
228+
229+
By("waiting for the curl-metrics pod to complete.")
230+
verifyCurlUp := func() error {
231+
cmd := exec.Command("kubectl", "get", "pods", "curl-metrics",
232+
"-o", "jsonpath={.status.phase}",
233+
"-n", namespace)
234+
status, err := utils.Run(cmd)
235+
ExpectWithOffset(3, err).NotTo(HaveOccurred())
236+
if string(status) != "Succeeded" {
237+
return fmt.Errorf("curl pod in %s status", status)
238+
}
239+
return nil
240+
}
241+
EventuallyWithOffset(2, verifyCurlUp, 5*time.Minute, 10*time.Second).Should(Succeed())
242+
243+
By("getting the metrics by checking curl-metrics logs")
244+
metricsOutput := getMetricsOutput()
245+
ExpectWithOffset(1, metricsOutput).To(ContainSubstring(
246+
"controller_runtime_reconcile_total",
247+
))
248+
})
249+
250+
// TODO: Customize the e2e test suite with scenarios specific to your project.
251+
// Consider applying sample/CR(s) and check their status and/or verifying
252+
// the reconciliation by using the metrics, i.e.:
253+
// metricsOutput := getMetricsOutput()
254+
// ExpectWithOffset(1, metricsOutput).To(ContainSubstring(
255+
// fmt.Sprintf(`controller_runtime_reconcile_total{controller="%s",result="success"} 1`,
256+
// strings.ToLower(<Kind>),
257+
// ))
154258
})
155259
})
260+
261+
// serviceAccountToken returns a token for the specified service account in the given namespace.
262+
// It uses the Kubernetes TokenRequest API to generate a token by directly sending a request
263+
// and parsing the resulting token from the API response.
264+
func serviceAccountToken() (string, error) {
265+
const tokenRequestRawString = `{
266+
"apiVersion": "authentication.k8s.io/v1",
267+
"kind": "TokenRequest"
268+
}`
269+
270+
// Temporary file to store the token request
271+
secretName := fmt.Sprintf("%s-token-request", serviceAccountName)
272+
tokenRequestFile := filepath.Join("/tmp", secretName)
273+
err := os.WriteFile(tokenRequestFile, []byte(tokenRequestRawString), os.FileMode(0o755))
274+
if err != nil {
275+
return "", err
276+
}
277+
278+
var out string
279+
var rawJson string
280+
Eventually(func() error {
281+
// Execute kubectl command to create the token
282+
cmd := exec.Command("kubectl", "create", "--raw", fmt.Sprintf(
283+
"/api/v1/namespaces/%s/serviceaccounts/%s/token",
284+
namespace,
285+
serviceAccountName,
286+
), "-f", tokenRequestFile)
287+
288+
output, err := cmd.CombinedOutput()
289+
if err != nil {
290+
return err
291+
}
292+
293+
rawJson = string(output)
294+
295+
// Parse the JSON output to extract the token
296+
var token tokenRequest
297+
err = json.Unmarshal([]byte(rawJson), &token)
298+
if err != nil {
299+
return err
300+
}
301+
302+
out = token.Status.Token
303+
return nil
304+
}, time.Minute, time.Second).Should(Succeed())
305+
306+
return out, err
307+
}
308+
309+
// getMetricsOutput retrieves and returns the logs from the curl pod used to access the metrics endpoint.
310+
func getMetricsOutput() string {
311+
By("getting the curl-metrics logs")
312+
cmd := exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace)
313+
metricsOutput, err := utils.Run(cmd)
314+
ExpectWithOffset(3, err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod")
315+
metricsOutputStr := string(metricsOutput)
316+
ExpectWithOffset(3, metricsOutputStr).To(ContainSubstring("< HTTP/1.1 200 OK"))
317+
return metricsOutputStr
318+
}
319+
320+
// tokenRequest is a simplified representation of the Kubernetes TokenRequest API response,
321+
// containing only the token field that we need to extract.
322+
type tokenRequest struct {
323+
Status struct {
324+
Token string `json:"token"`
325+
} `json:"status"`
326+
}

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 enable")
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)