@@ -5,11 +5,15 @@ package e2e
55
66import (
77 "context"
8+ "encoding/json"
89 "flag"
10+ "fmt"
911 "os"
12+ "os/exec"
1013 "path/filepath"
1114 "strings"
1215 "testing"
16+ "time"
1317
1418 metal3api "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"
1519 . "github.com/onsi/ginkgo/v2"
@@ -79,6 +83,69 @@ func TestE2e(t *testing.T) {
7983 RunSpecs (t , "E2e Suite" )
8084}
8185
86+ const namespace = "baremetal-operator-system"
87+ const serviceAccountName = "baremetal-operator-controller-manager"
88+ const metricsServiceName = "baremetal-operator-controller-manager-metrics-service"
89+ const metricsRoleBindingName = "baremetal-operator-metrics-binding"
90+
91+ // serviceAccountToken returns a token for the specified service account in the given namespace.
92+ // It uses the Kubernetes TokenRequest API to generate a token by directly sending a request
93+ // and parsing the resulting token from the API response.
94+ func serviceAccountToken () (string , error ) {
95+ const tokenRequestRawString = `{
96+ "apiVersion": "authentication.k8s.io/v1",
97+ "kind": "TokenRequest"
98+ }`
99+
100+ // Temporary file to store the token request
101+ secretName := fmt .Sprintf ("%s-token-request" , serviceAccountName )
102+ tokenRequestFile := filepath .Join ("/tmp" , secretName ) //nolint: gocritic
103+ err := os .WriteFile (tokenRequestFile , []byte (tokenRequestRawString ), os .FileMode (0o644 ))
104+ if err != nil {
105+ return "" , err
106+ }
107+
108+ var out string
109+ verifyTokenCreation := func (g Gomega ) {
110+ // Execute kubectl command to create the token
111+ cmd := exec .Command ("kubectl" , "create" , "--raw" , fmt .Sprintf (
112+ "/api/v1/namespaces/%s/serviceaccounts/%s/token" ,
113+ namespace ,
114+ serviceAccountName ,
115+ ), "-f" , tokenRequestFile )
116+
117+ output , err := cmd .CombinedOutput ()
118+ g .Expect (err ).NotTo (HaveOccurred ())
119+
120+ // Parse the JSON output to extract the token
121+ var token tokenRequest
122+ err = json .Unmarshal (output , & token )
123+ g .Expect (err ).NotTo (HaveOccurred ())
124+
125+ out = token .Status .Token
126+ }
127+ Eventually (verifyTokenCreation ).Should (Succeed ())
128+
129+ return out , err
130+ }
131+
132+ // tokenRequest is a simplified representation of the Kubernetes TokenRequest API response,
133+ // containing only the token field that we need to extract.
134+ type tokenRequest struct {
135+ Status struct {
136+ Token string `json:"token"`
137+ } `json:"status"`
138+ }
139+
140+ // getMetricsOutput retrieves and returns the logs from the curl pod used to access the metrics endpoint.
141+ func getMetricsOutput () string {
142+ By ("getting the curl-metrics logs" )
143+ cmd := exec .Command ("kubectl" , "logs" , "curl-metrics" , "-n" , namespace )
144+ metricsOutput , err := cmd .CombinedOutput ()
145+ Expect (err ).NotTo (HaveOccurred (), "Failed to retrieve logs from curl pod" )
146+ return string (metricsOutput )
147+ }
148+
82149var _ = SynchronizedBeforeSuite (func () []byte {
83150 var kubeconfigPath string
84151
@@ -161,6 +228,62 @@ var _ = SynchronizedBeforeSuite(func() []byte {
161228 Expect (err ).NotTo (HaveOccurred ())
162229 }
163230
231+ // Metrics test start
232+ By ("creating a ClusterRoleBinding for the service account to allow access to metrics" )
233+ cmd := exec .Command ("kubectl" , "create" , "clusterrolebinding" , metricsRoleBindingName ,
234+ "--clusterrole=baremetal-operator-metrics-reader" ,
235+ fmt .Sprintf ("--serviceaccount=%s:%s" , namespace , serviceAccountName ),
236+ )
237+ _ , err := cmd .CombinedOutput ()
238+ Expect (err ).NotTo (HaveOccurred (), "Failed to create ClusterRoleBinding" )
239+
240+ By ("validating that the metrics service is available" )
241+ cmd = exec .Command ("kubectl" , "get" , "service" , metricsServiceName , "-n" , namespace )
242+ _ , err = cmd .CombinedOutput ()
243+ Expect (err ).NotTo (HaveOccurred (), "Metrics service should exist" )
244+
245+ By ("getting the service account token" )
246+ token , err := serviceAccountToken ()
247+ Expect (err ).NotTo (HaveOccurred ())
248+ Expect (token ).NotTo (BeEmpty ())
249+
250+ By ("waiting for the metrics endpoint to be ready" )
251+ verifyMetricsEndpointReady := func (g Gomega ) {
252+ cmd := exec .Command ("kubectl" , "get" , "endpoints" , metricsServiceName , "-n" , namespace )
253+ output , err := cmd .CombinedOutput ()
254+ g .Expect (err ).NotTo (HaveOccurred ())
255+ g .Expect (output ).To (ContainSubstring ("8443" ), "Metrics endpoint is not ready" )
256+ }
257+ Eventually (verifyMetricsEndpointReady ).Should (Succeed ())
258+
259+ By ("creating the curl-metrics pod to access the metrics endpoint" )
260+ cmd = exec .Command ("kubectl" , "run" , "curl-metrics" , "--restart=Never" ,
261+ "--namespace" , namespace ,
262+ "--image=curlimages/curl:7.87.0" ,
263+ "--command" ,
264+ "--" , "curl" , "-v" , "--tlsv1.3" , "-k" , "-H" , fmt .Sprintf ("Authorization:Bearer %s" , token ),
265+ fmt .Sprintf ("https://%s.%s.svc.cluster.local:8443/metrics" , metricsServiceName , namespace ))
266+ _ , err = cmd .CombinedOutput ()
267+ Expect (err ).NotTo (HaveOccurred (), "Failed to create curl-metrics pod" )
268+
269+ By ("waiting for the curl-metrics pod to complete." )
270+ verifyCurlUp := func (g Gomega ) {
271+ cmd := exec .Command ("kubectl" , "get" , "pods" , "curl-metrics" ,
272+ "-o" , "jsonpath={.status.phase}" ,
273+ "-n" , namespace )
274+ output , err := cmd .CombinedOutput ()
275+ g .Expect (err ).NotTo (HaveOccurred ())
276+ g .Expect (string (output )).To (Equal ("Succeeded" ), "curl pod in wrong status" )
277+ }
278+ Eventually (verifyCurlUp , 5 * time .Minute ).Should (Succeed ())
279+
280+ By ("getting the metrics by checking curl-metrics logs" )
281+ metricsOutput := getMetricsOutput ()
282+ Expect (metricsOutput ).To (ContainSubstring (
283+ "controller_runtime_reconcile_total" ,
284+ ))
285+ // Metrics test end
286+
164287 return []byte (strings .Join ([]string {clusterProxy .GetKubeconfigPath ()}, "," ))
165288}, func (data []byte ) {
166289 // Before each parallel node
0 commit comments