Skip to content

Commit a6c6a15

Browse files
committed
Add e2e test for metrics service
Signed-off-by: Kashif Khan <kashif.khan@est.tech>
1 parent d0c18a6 commit a6c6a15

5 files changed

Lines changed: 180 additions & 47 deletions

File tree

.golangci.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ issues:
120120
linters:
121121
- gci
122122
- goconst
123+
- gosec
123124
- path: _test\.go
124125
linters:
125126
- errcheck

config/base/manager.yaml

Lines changed: 50 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -19,52 +19,56 @@ spec:
1919
webhook: metal3-io-v1alpha1-baremetalhost
2020
spec:
2121
containers:
22-
- command:
23-
- /baremetal-operator
24-
args:
25-
- --enable-leader-election
26-
- --tls-min-version=TLS13
27-
image: quay.io/metal3-io/baremetal-operator
28-
imagePullPolicy: Always
29-
env:
30-
- name: POD_NAME
31-
valueFrom:
32-
fieldRef:
33-
fieldPath: metadata.name
34-
- name: POD_NAMESPACE
35-
valueFrom:
36-
fieldRef:
37-
fieldPath: metadata.namespace
38-
envFrom:
39-
- configMapRef:
40-
name: ironic
41-
name: manager
42-
securityContext:
43-
allowPrivilegeEscalation: false
44-
capabilities:
45-
drop:
46-
- ALL
47-
privileged: false
48-
runAsUser: 65532
49-
runAsGroup: 65532
50-
livenessProbe:
51-
httpGet:
52-
path: /healthz
53-
port: 9440
54-
initialDelaySeconds: 10
55-
periodSeconds: 10
56-
timeoutSeconds: 2
57-
successThreshold: 1
58-
failureThreshold: 10
59-
readinessProbe:
60-
httpGet:
61-
path: /readyz
62-
port: 9440
63-
initialDelaySeconds: 10
64-
periodSeconds: 10
65-
timeoutSeconds: 2
66-
successThreshold: 1
67-
failureThreshold: 10
22+
- command:
23+
- /baremetal-operator
24+
args:
25+
- --enable-leader-election
26+
- --tls-min-version=TLS13
27+
ports:
28+
- containerPort: 8443
29+
protocol: TCP
30+
name: https
31+
image: quay.io/metal3-io/baremetal-operator
32+
imagePullPolicy: Always
33+
env:
34+
- name: POD_NAME
35+
valueFrom:
36+
fieldRef:
37+
fieldPath: metadata.name
38+
- name: POD_NAMESPACE
39+
valueFrom:
40+
fieldRef:
41+
fieldPath: metadata.namespace
42+
envFrom:
43+
- configMapRef:
44+
name: ironic
45+
name: manager
46+
securityContext:
47+
allowPrivilegeEscalation: false
48+
capabilities:
49+
drop:
50+
- ALL
51+
privileged: false
52+
runAsUser: 65532
53+
runAsGroup: 65532
54+
livenessProbe:
55+
httpGet:
56+
path: /healthz
57+
port: 9440
58+
initialDelaySeconds: 10
59+
periodSeconds: 10
60+
timeoutSeconds: 2
61+
successThreshold: 1
62+
failureThreshold: 10
63+
readinessProbe:
64+
httpGet:
65+
path: /readyz
66+
port: 9440
67+
initialDelaySeconds: 10
68+
periodSeconds: 10
69+
timeoutSeconds: 2
70+
successThreshold: 1
71+
failureThreshold: 10
6872
terminationGracePeriodSeconds: 10
6973
securityContext:
7074
runAsNonRoot: true

config/render/capm3.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2598,6 +2598,9 @@ spec:
25982598
- containerPort: 9443
25992599
name: webhook-server
26002600
protocol: TCP
2601+
- containerPort: 8443
2602+
name: https
2603+
protocol: TCP
26012604
readinessProbe:
26022605
failureThreshold: 10
26032606
httpGet:

main.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ func main() {
137137
// namespace.
138138
flag.StringVar(&watchNamespace, "namespace", os.Getenv("WATCH_NAMESPACE"),
139139
"Namespace that the controller watches to reconcile host resources.")
140-
flag.StringVar(&metricsBindAddr, "metrics-addr", "127.0.0.1:8085",
140+
flag.StringVar(&metricsBindAddr, "metrics-addr", ":8443",
141141
"The address the metric endpoint binds to.")
142142
flag.BoolVar(&enableLeaderElection, "enable-leader-election", false,
143143
"Enable leader election for controller manager. "+
@@ -217,7 +217,9 @@ func main() {
217217
Scheme: scheme,
218218
Metrics: metricsserver.Options{
219219
BindAddress: metricsBindAddr,
220+
SecureServing: true,
220221
FilterProvider: filters.WithAuthenticationAndAuthorization,
222+
TLSOpts: tlsOptionOverrides,
221223
},
222224
WebhookServer: webhook.NewServer(webhook.Options{
223225
Port: webhookPort,

test/e2e/e2e_suite_test.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,15 @@ package e2e
55

66
import (
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+
82149
var _ = 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

Comments
 (0)