Skip to content

Commit

Permalink
✨ add metrics check and further helpers for the e2e tests
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
camilamacedo86 committed Sep 2, 2024
1 parent d4fac4c commit 69c1d84
Show file tree
Hide file tree
Showing 19 changed files with 1,264 additions and 47 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/test-e2e-samples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ jobs:
run: |
KUSTOMIZATION_FILE_PATH="testdata/project-v4/config/default/kustomization.yaml"
sed -i '25s/^#//' $KUSTOMIZATION_FILE_PATH
sed -i '27s/^#//' $KUSTOMIZATION_FILE_PATH
sed -i '51s/^#//' $KUSTOMIZATION_FILE_PATH
sed -i '55,151s/^#//' $KUSTOMIZATION_FILE_PATH
cd testdata/project-v4/
Expand All @@ -58,6 +59,7 @@ jobs:
run: |
KUSTOMIZATION_FILE_PATH="testdata/project-v4-with-plugins/config/default/kustomization.yaml"
sed -i '25s/^#//' $KUSTOMIZATION_FILE_PATH
sed -i '27s/^#//' $KUSTOMIZATION_FILE_PATH
sed -i '51s/^#//' $KUSTOMIZATION_FILE_PATH
# Uncomment only ValidatingWebhookConfiguration
# from cert-manager replaces
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ func TestE2E(t *testing.T) {
}

var _ = BeforeSuite(func() {
By("Ensure that Prometheus is enable")
_ = utils.UncommentCode("config/default/kustomization.yaml", "#- ../prometheus", "#")

By("generating files")
cmd := exec.Command("make", "generate")
_, err := utils.Run(cmd)
Expand Down
163 changes: 156 additions & 7 deletions docs/book/src/cronjob-tutorial/testdata/project/test/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ limitations under the License.
package e2e

import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"time"

. "github.com/onsi/ginkgo/v2"
Expand All @@ -27,10 +30,19 @@ import (
"tutorial.kubebuilder.io/project/test/utils"
)

// namespace where the project is deployed in
const namespace = "project-system"

// Define a set of end-to-end (e2e) tests to validate the behavior of the controller.
var _ = Describe("controller", Ordered, func() {
// serviceAccountName created for the project
const serviceAccountName = "project-controller-manager"

// metricsServiceName is the name of the metrics service of the project
const metricsServiceName = "project-controller-manager-metrics-service"

// metricsRoleBindingName is the name of the RBAC that will be created to allow get the metrics data
const metricsRoleBindingName = "project-metrics-binding"

var _ = Describe("Manager", Ordered, func() {
// Before running the tests, set up the environment by creating the namespace,
// installing CRDs, and deploying the controller.
BeforeAll(func() {
Expand All @@ -53,8 +65,12 @@ var _ = Describe("controller", Ordered, func() {
// After all tests have been executed, clean up by undeploying the controller, uninstalling CRDs,
// and deleting the namespace.
AfterAll(func() {
By("cleaning up the curl pod for metrics")
cmd := exec.Command("kubectl", "delete", "pod", "curl-metrics", "-n", namespace)
_, _ = utils.Run(cmd)

By("undeploying the controller-manager")
cmd := exec.Command("make", "undeploy")
cmd = exec.Command("make", "undeploy")
_, _ = utils.Run(cmd)

By("uninstalling CRDs")
Expand All @@ -66,8 +82,8 @@ var _ = Describe("controller", Ordered, func() {
_, _ = utils.Run(cmd)
})

// The Context block contains the actual tests that validate the operator's behavior.
Context("Operator", func() {
// The Context block contains the actual tests that validate the manager's behavior.
Context("Manager", func() {
It("should run successfully", func() {
var controllerPodName string

Expand Down Expand Up @@ -149,7 +165,140 @@ var _ = Describe("controller", Ordered, func() {

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

// TODO(user): Customize the e2e test suite to include
// additional scenarios specific to your project.
It("should ensure the metrics endpoint is serving metrics", func() {
// Step 1: Create a ClusterRoleBinding for the service account to allow access to metrics
cmd := exec.Command("kubectl", "create", "clusterrolebinding", metricsRoleBindingName,
"--clusterrole=project-metrics-reader",
fmt.Sprintf("--serviceaccount=%s:%s", namespace, serviceAccountName),
)
_, err := utils.Run(cmd)
ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to create ClusterRoleBinding")

// Step 2: Ensure the metrics service is available
By("validating that the metrics service is available")
cmd = exec.Command("kubectl", "get", "service", metricsServiceName, "-n", namespace)
_, err = utils.Run(cmd)
ExpectWithOffset(2, err).NotTo(HaveOccurred(), "Metrics service should exist")

// Step 3: Validate that the ServiceMonitor for Prometheus is applied in the namespace
By("validating that the ServiceMonitor for Prometheus is applied in the namespace")
cmd = exec.Command("kubectl", "get", "ServiceMonitor", "-n", namespace)
_, err = utils.Run(cmd)
ExpectWithOffset(2, err).NotTo(HaveOccurred(), "ServiceMonitor should exist")

// Step 4: Get the service account token
token, err := serviceAccountToken()
ExpectWithOffset(2, err).NotTo(HaveOccurred())
ExpectWithOffset(2, token).NotTo(BeEmpty())

// Step 5: Create a curl pod to access the metrics endpoint using the token
By("creating a curl pod to access the metrics endpoint")
cmd = exec.Command("kubectl", "run", "curl-metrics", "--restart=Never",
"--namespace", namespace,
"--image=curlimages/curl:7.78.0",
"--", "/bin/sh", "-c", fmt.Sprintf(
"curl -v -k -H 'Authorization: Bearer %s' https://%s.%s.svc.cluster.local:8443/metrics",
token, metricsServiceName, namespace))
_, err = utils.Run(cmd)
ExpectWithOffset(2, err).NotTo(HaveOccurred(), "Failed to create curl pod")

// Step 6: Wait for the curl pod to complete. We need to use the pod to get the metrics data
By("validating that the curl pod is running as expected")
verifyCurlUp := func() error {
cmd := exec.Command("kubectl", "get", "pods", "curl-metrics",
"-o", "jsonpath={.status.phase}",
"-n", namespace)
status, err := utils.Run(cmd)
ExpectWithOffset(3, err).NotTo(HaveOccurred())
if string(status) != "Succeeded" {
return fmt.Errorf("curl pod in %s status", status)
}
return nil
}
EventuallyWithOffset(2, verifyCurlUp, 240*time.Second, time.Second).Should(Succeed())

// Step 7: Retrieve and validate the metrics output
By("getting the curl-metrics logs")
metricsOutput := getMetricsOutput()
ExpectWithOffset(1, metricsOutput).To(ContainSubstring(
"controller_runtime_reconcile_total",
))
})

// TODO: Customize the e2e test suite with scenarios specific to your project.
// Consider applying sample/CR(s) and check their status and/or verifying
// the reconciliation by using the metrics, i.e.:
// metricsOutput := getMetricsOutput()
// ExpectWithOffset(1, metricsOutput).To(ContainSubstring(
// fmt.Sprintf(`controller_runtime_reconcile_total{controller="%s",result="success"} 1`,
// strings.ToLower(<Kind>),
// ))
})
})

// serviceAccountToken returns a token for the specified service account in the given namespace.
// It uses the Kubernetes TokenRequest API to generate a token by directly sending a request
// and parsing the resulting token from the API response.
func serviceAccountToken() (string, error) {
const tokenRequestRawString = `{
"apiVersion": "authentication.k8s.io/v1",
"kind": "TokenRequest"
}`

// Temporary file to store the token request
secretName := fmt.Sprintf("%s-token-request", serviceAccountName)
tokenRequestFile := filepath.Join("/tmp", secretName)
err := os.WriteFile(tokenRequestFile, []byte(tokenRequestRawString), os.FileMode(0o755))
if err != nil {
return "", err
}

var out string
var rawJson string
Eventually(func() error {
// Execute kubectl command to create the token
cmd := exec.Command("kubectl", "create", "--raw", fmt.Sprintf(
"/api/v1/namespaces/%s/serviceaccounts/%s/token",
namespace,
serviceAccountName,
), "-f", tokenRequestFile)

output, err := cmd.CombinedOutput()
if err != nil {
return err
}

rawJson = string(output)

// Parse the JSON output to extract the token
var token tokenRequest
err = json.Unmarshal([]byte(rawJson), &token)
if err != nil {
return err
}

out = token.Status.Token
return nil
}, time.Minute, time.Second).Should(Succeed())

return out, err
}

// getMetricsOutput retrieves and returns the logs from the curl pod used to access the metrics endpoint.
func getMetricsOutput() string {
By("getting the curl-metrics logs")
cmd := exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace)
metricsOutput, err := utils.Run(cmd)
ExpectWithOffset(3, err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod")
metricsOutputStr := string(metricsOutput)
ExpectWithOffset(3, metricsOutputStr).To(ContainSubstring("< HTTP/1.1 200 OK"))
return metricsOutputStr
}

// tokenRequest is a simplified representation of the Kubernetes TokenRequest API response,
// containing only the token field that we need to extract.
type tokenRequest struct {
Status struct {
Token string `json:"token"`
} `json:"status"`
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ limitations under the License.
package utils

import (
"bufio"
"bytes"
"fmt"
"os"
"os/exec"
Expand Down Expand Up @@ -198,3 +200,52 @@ func GetProjectDir() (string, error) {
wd = strings.Replace(wd, "/test/e2e", "", -1)
return wd, nil
}

// UncommentCode searches for target in the file and remove the comment prefix
// of the target content. The target content may span multiple lines.
func UncommentCode(filename, target, prefix string) error {
// false positive
// nolint:gosec
content, err := os.ReadFile(filename)
if err != nil {
return err
}
strContent := string(content)

idx := strings.Index(strContent, target)
if idx < 0 {
return fmt.Errorf("unable to find the code %s to be uncomment", target)
}

out := new(bytes.Buffer)
_, err = out.Write(content[:idx])
if err != nil {
return err
}

scanner := bufio.NewScanner(bytes.NewBufferString(target))
if !scanner.Scan() {
return nil
}
for {
_, err := out.WriteString(strings.TrimPrefix(scanner.Text(), prefix))
if err != nil {
return err
}
// Avoid writing a newline in case the previous line was the last in target.
if !scanner.Scan() {
break
}
if _, err := out.WriteString("\n"); err != nil {
return err
}
}

_, err = out.Write(content[idx+len(target):])
if err != nil {
return err
}
// false positive
// nolint:gosec
return os.WriteFile(filename, out.Bytes(), 0644)
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ func TestE2E(t *testing.T) {
}

var _ = BeforeSuite(func() {
By("Ensure that Prometheus is enable")
_ = utils.UncommentCode("config/default/kustomization.yaml", "#- ../prometheus", "#")

By("generating files")
cmd := exec.Command("make", "generate")
_, err := utils.Run(cmd)
Expand Down
Loading

0 comments on commit 69c1d84

Please sign in to comment.