@@ -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
@@ -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\t Serving 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+ }
0 commit comments