@@ -17,8 +17,12 @@ limitations under the License.
1717package e2e
1818
1919import (
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
3035const 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
@@ -110,5 +126,163 @@ var _ = Describe("controller", Ordered, func() {
110126
111127 // TODO(user): Customize the e2e test suite to include
112128 // additional scenarios specific to your project.
129+
130+ It ("should ensure the metrics endpoint is serving metrics" , func () {
131+ By ("creating a ClusterRoleBinding for the service account to allow access to metrics" )
132+ cmd := exec .Command ("kubectl" , "create" , "clusterrolebinding" , metricsRoleBindingName ,
133+ "--clusterrole=project-metrics-reader" ,
134+ fmt .Sprintf ("--serviceaccount=%s:%s" , namespace , serviceAccountName ),
135+ )
136+ _ , err := utils .Run (cmd )
137+ ExpectWithOffset (1 , err ).NotTo (HaveOccurred (), "Failed to create ClusterRoleBinding" )
138+
139+ By ("validating that the metrics service is available" )
140+ cmd = exec .Command ("kubectl" , "get" , "service" , metricsServiceName , "-n" , namespace )
141+ _ , err = utils .Run (cmd )
142+ ExpectWithOffset (2 , err ).NotTo (HaveOccurred (), "Metrics service should exist" )
143+
144+ By ("validating that the ServiceMonitor for Prometheus is applied in the namespace" )
145+ cmd = exec .Command ("kubectl" , "get" , "ServiceMonitor" , "-n" , namespace )
146+ _ , err = utils .Run (cmd )
147+ ExpectWithOffset (2 , err ).NotTo (HaveOccurred (), "ServiceMonitor should exist" )
148+
149+ By ("getting the service account token" )
150+ token , err := serviceAccountToken ()
151+ ExpectWithOffset (2 , err ).NotTo (HaveOccurred ())
152+ ExpectWithOffset (2 , token ).NotTo (BeEmpty ())
153+
154+ By ("waiting for the metrics endpoint to be ready" )
155+ verifyMetricsEndpointReady := func () error {
156+ cmd := exec .Command ("kubectl" , "get" , "endpoints" , metricsServiceName , "-n" , namespace )
157+ output , err := utils .Run (cmd )
158+ if err != nil {
159+ return err
160+ }
161+ if ! strings .Contains (string (output ), "8443" ) {
162+ return fmt .Errorf ("metrics endpoint is not ready" )
163+ }
164+ return nil
165+ }
166+ EventuallyWithOffset (2 , verifyMetricsEndpointReady , 2 * time .Minute , 10 * time .Second ).Should (Succeed ())
167+
168+ By ("verifying that the controller manager is serving the metrics server" )
169+ Eventually (func () error {
170+ cmd := exec .Command ("kubectl" , "logs" , controllerPodName , "-n" , namespace )
171+ logs , err := utils .Run (cmd )
172+ if err != nil {
173+ return err
174+ }
175+ if ! strings .Contains (string (logs ), "controller-runtime.metrics\t Serving metrics server" ) {
176+ return fmt .Errorf ("metrics server not yet started" )
177+ }
178+ return nil
179+ }, 2 * time .Minute , 10 * time .Second ).Should (Succeed (), "Controller manager did not start serving metrics server" )
180+
181+ By ("creating the curl-metrics pod to access the metrics endpoint" )
182+ cmd = exec .Command ("kubectl" , "run" , "curl-metrics" , "--restart=Never" ,
183+ "--namespace" , namespace ,
184+ "--image=curlimages/curl:7.78.0" ,
185+ "--" , "/bin/sh" , "-c" , fmt .Sprintf (
186+ "curl -v -k -H 'Authorization: Bearer %s' https://%s.%s.svc.cluster.local:8443/metrics" ,
187+ token , metricsServiceName , namespace ))
188+ _ , err = utils .Run (cmd )
189+ ExpectWithOffset (1 , err ).NotTo (HaveOccurred (), "Failed to create curl-metrics pod" )
190+
191+ By ("waiting for the curl-metrics pod to complete." )
192+ verifyCurlUp := func () error {
193+ cmd := exec .Command ("kubectl" , "get" , "pods" , "curl-metrics" ,
194+ "-o" , "jsonpath={.status.phase}" ,
195+ "-n" , namespace )
196+ status , err := utils .Run (cmd )
197+ ExpectWithOffset (3 , err ).NotTo (HaveOccurred ())
198+ if string (status ) != "Succeeded" {
199+ return fmt .Errorf ("curl pod in %s status" , status )
200+ }
201+ return nil
202+ }
203+ EventuallyWithOffset (2 , verifyCurlUp , 5 * time .Minute , 10 * time .Second ).Should (Succeed ())
204+
205+ By ("getting the metrics by checking curl-metrics logs" )
206+ metricsOutput := getMetricsOutput ()
207+ ExpectWithOffset (1 , metricsOutput ).To (ContainSubstring (
208+ "controller_runtime_reconcile_total" ,
209+ ))
210+ })
211+
212+ // TODO: Customize the e2e test suite with scenarios specific to your project.
213+ // Consider applying sample/CR(s) and check their status and/or verifying
214+ // the reconciliation by using the metrics, i.e.:
215+ // metricsOutput := getMetricsOutput()
216+ // ExpectWithOffset(1, metricsOutput).To(ContainSubstring(
217+ // fmt.Sprintf(`controller_runtime_reconcile_total{controller="%s",result="success"} 1`,
218+ // strings.ToLower(<Kind>),
219+ // ))
113220 })
114221})
222+
223+ // serviceAccountToken returns a token for the specified service account in the given namespace.
224+ // It uses the Kubernetes TokenRequest API to generate a token by directly sending a request
225+ // and parsing the resulting token from the API response.
226+ func serviceAccountToken () (string , error ) {
227+ const tokenRequestRawString = `{
228+ "apiVersion": "authentication.k8s.io/v1",
229+ "kind": "TokenRequest"
230+ }`
231+
232+ // Temporary file to store the token request
233+ secretName := fmt .Sprintf ("%s-token-request" , serviceAccountName )
234+ tokenRequestFile := filepath .Join ("/tmp" , secretName )
235+ err := os .WriteFile (tokenRequestFile , []byte (tokenRequestRawString ), os .FileMode (0o755 ))
236+ if err != nil {
237+ return "" , err
238+ }
239+
240+ var out string
241+ var rawJson string
242+ Eventually (func () error {
243+ // Execute kubectl command to create the token
244+ cmd := exec .Command ("kubectl" , "create" , "--raw" , fmt .Sprintf (
245+ "/api/v1/namespaces/%s/serviceaccounts/%s/token" ,
246+ namespace ,
247+ serviceAccountName ,
248+ ), "-f" , tokenRequestFile )
249+
250+ output , err := cmd .CombinedOutput ()
251+ if err != nil {
252+ return err
253+ }
254+
255+ rawJson = string (output )
256+
257+ // Parse the JSON output to extract the token
258+ var token tokenRequest
259+ err = json .Unmarshal ([]byte (rawJson ), & token )
260+ if err != nil {
261+ return err
262+ }
263+
264+ out = token .Status .Token
265+ return nil
266+ }, time .Minute , time .Second ).Should (Succeed ())
267+
268+ return out , err
269+ }
270+
271+ // getMetricsOutput retrieves and returns the logs from the curl pod used to access the metrics endpoint.
272+ func getMetricsOutput () string {
273+ By ("getting the curl-metrics logs" )
274+ cmd := exec .Command ("kubectl" , "logs" , "curl-metrics" , "-n" , namespace )
275+ metricsOutput , err := utils .Run (cmd )
276+ ExpectWithOffset (3 , err ).NotTo (HaveOccurred (), "Failed to retrieve logs from curl pod" )
277+ metricsOutputStr := string (metricsOutput )
278+ ExpectWithOffset (3 , metricsOutputStr ).To (ContainSubstring ("< HTTP/1.1 200 OK" ))
279+ return metricsOutputStr
280+ }
281+
282+ // tokenRequest is a simplified representation of the Kubernetes TokenRequest API response,
283+ // containing only the token field that we need to extract.
284+ type tokenRequest struct {
285+ Status struct {
286+ Token string `json:"token"`
287+ } `json:"status"`
288+ }
0 commit comments