|
| 1 | +// Package e2e contains end-to-end tests to verify that the metrics endpoints |
| 2 | +// for both components. Metrics are exported and accessible by authorized users through |
| 3 | +// RBAC and ServiceAccount tokens. |
| 4 | +// |
| 5 | +// These tests perform the following steps: |
| 6 | +// 1. Create a ClusterRoleBinding to grant necessary permissions for accessing metrics. |
| 7 | +// 2. Generate a ServiceAccount token for authentication. |
| 8 | +// 3. Deploy a curl pod to interact with the metrics endpoint. |
| 9 | +// 4. Wait for the curl pod to become ready. |
| 10 | +// 5. Execute a curl command from the pod to validate the metrics endpoint. |
| 11 | +// 6. Clean up all resources created during the test, such as the ClusterRoleBinding and curl pod. |
| 12 | +// |
| 13 | +//nolint:gosec |
1 | 14 | package e2e |
2 | 15 |
|
3 | 16 | import ( |
4 | 17 | "bytes" |
5 | 18 | "io" |
6 | 19 | "os/exec" |
| 20 | + "strings" |
7 | 21 | "testing" |
8 | 22 |
|
9 | 23 | "github.com/stretchr/testify/require" |
| 24 | + |
| 25 | + "github.com/operator-framework/operator-controller/test/utils" |
10 | 26 | ) |
11 | 27 |
|
12 | | -// nolint:gosec |
13 | 28 | // TestOperatorControllerMetricsExportedEndpoint verifies that the metrics endpoint for the operator controller |
14 | | -// is exported correctly and accessible by authorized users through RBAC and a ServiceAccount token. |
15 | | -// The test performs the following steps: |
16 | | -// 1. Creates a ClusterRoleBinding to grant necessary permissions for accessing metrics. |
17 | | -// 2. Generates a ServiceAccount token for authentication. |
18 | | -// 3. Deploys a curl pod to interact with the metrics endpoint. |
19 | | -// 4. Waits for the curl pod to become ready. |
20 | | -// 5. Executes a curl command from the pod to validate the metrics endpoint. |
21 | | -// 6. Cleans up all resources created during the test, such as the ClusterRoleBinding and curl pod. |
22 | 29 | func TestOperatorControllerMetricsExportedEndpoint(t *testing.T) { |
23 | | - var ( |
24 | | - token string |
25 | | - curlPod = "curl-metrics" |
26 | | - client = "" |
27 | | - clients = []string{"kubectl", "oc"} |
| 30 | + client := utils.FindK8sClient(t) |
| 31 | + config := NewMetricsTestConfig( |
| 32 | + t, client, |
| 33 | + "control-plane=operator-controller-controller-manager", |
| 34 | + "operator-controller-metrics-reader", |
| 35 | + "operator-controller-metrics-binding", |
| 36 | + "operator-controller-controller-manager", |
| 37 | + "oper-curl-metrics", |
| 38 | + "https://operator-controller-service.NAMESPACE.svc.cluster.local:8443/metrics", |
28 | 39 | ) |
29 | 40 |
|
30 | | - t.Log("Looking for k8s client") |
31 | | - for _, c := range clients { |
32 | | - // Would prefer to use `command -v`, but even that may not be installed! |
33 | | - err := exec.Command(c, "version", "--client").Run() |
34 | | - if err == nil { |
35 | | - client = c |
36 | | - break |
37 | | - } |
38 | | - } |
39 | | - if client == "" { |
40 | | - t.Fatal("k8s client not found") |
| 41 | + config.run() |
| 42 | +} |
| 43 | + |
| 44 | +// TestCatalogdMetricsExportedEndpoint verifies that the metrics endpoint for catalogd |
| 45 | +func TestCatalogdMetricsExportedEndpoint(t *testing.T) { |
| 46 | + client := utils.FindK8sClient(t) |
| 47 | + config := NewMetricsTestConfig( |
| 48 | + t, client, |
| 49 | + "control-plane=catalogd-controller-manager", |
| 50 | + "catalogd-metrics-reader", |
| 51 | + "catalogd-metrics-binding", |
| 52 | + "catalogd-controller-manager", |
| 53 | + "catalogd-curl-metrics", |
| 54 | + "https://catalogd-service.NAMESPACE.svc.cluster.local:7443/metrics", |
| 55 | + ) |
| 56 | + |
| 57 | + config.run() |
| 58 | +} |
| 59 | + |
| 60 | +// MetricsTestConfig holds the necessary configurations for testing metrics endpoints. |
| 61 | +type MetricsTestConfig struct { |
| 62 | + t *testing.T |
| 63 | + client string |
| 64 | + namespace string |
| 65 | + clusterRole string |
| 66 | + clusterBinding string |
| 67 | + serviceAccount string |
| 68 | + curlPodName string |
| 69 | + metricsURL string |
| 70 | +} |
| 71 | + |
| 72 | +// NewMetricsTestConfig initializes a new MetricsTestConfig. |
| 73 | +func NewMetricsTestConfig(t *testing.T, client, selector, clusterRole, clusterBinding, serviceAccount, curlPodName, metricsURL string) *MetricsTestConfig { |
| 74 | + namespace := getComponentNamespace(t, client, selector) |
| 75 | + metricsURL = strings.ReplaceAll(metricsURL, "NAMESPACE", namespace) |
| 76 | + |
| 77 | + return &MetricsTestConfig{ |
| 78 | + t: t, |
| 79 | + client: client, |
| 80 | + namespace: namespace, |
| 81 | + clusterRole: clusterRole, |
| 82 | + clusterBinding: clusterBinding, |
| 83 | + serviceAccount: serviceAccount, |
| 84 | + curlPodName: curlPodName, |
| 85 | + metricsURL: metricsURL, |
41 | 86 | } |
42 | | - t.Logf("Using %q as k8s client", client) |
| 87 | +} |
43 | 88 |
|
44 | | - t.Log("Determining operator-controller namespace") |
45 | | - cmd := exec.Command(client, "get", "pods", "--all-namespaces", "--selector=control-plane=operator-controller-controller-manager", "--output=jsonpath={.items[0].metadata.namespace}") |
| 89 | +// run will execute all steps of those tests |
| 90 | +func (c *MetricsTestConfig) run() { |
| 91 | + c.createMetricsClusterRoleBinding() |
| 92 | + token := c.getServiceAccountToken() |
| 93 | + c.createCurlMetricsPod() |
| 94 | + c.validate(token) |
| 95 | + defer c.cleanup() |
| 96 | +} |
| 97 | + |
| 98 | +// createMetricsClusterRoleBinding to binding and expose the metrics |
| 99 | +func (c *MetricsTestConfig) createMetricsClusterRoleBinding() { |
| 100 | + c.t.Logf("Creating ClusterRoleBinding %s in namespace %s", c.clusterBinding, c.namespace) |
| 101 | + cmd := exec.Command(c.client, "create", "clusterrolebinding", c.clusterBinding, |
| 102 | + "--clusterrole="+c.clusterRole, |
| 103 | + "--serviceaccount="+c.namespace+":"+c.serviceAccount) |
46 | 104 | output, err := cmd.CombinedOutput() |
47 | | - require.NoError(t, err, "Error creating determining operator-controller namespace: %s", string(output)) |
48 | | - namespace := string(output) |
49 | | - if namespace == "" { |
50 | | - t.Fatal("No operator-controller namespace found") |
51 | | - } |
52 | | - t.Logf("Using %q as operator-controller namespace", namespace) |
53 | | - |
54 | | - t.Log("Creating ClusterRoleBinding for operator controller metrics") |
55 | | - cmd = exec.Command(client, "create", "clusterrolebinding", "operator-controller-metrics-binding", |
56 | | - "--clusterrole=operator-controller-metrics-reader", |
57 | | - "--serviceaccount="+namespace+":operator-controller-controller-manager") |
58 | | - output, err = cmd.CombinedOutput() |
59 | | - require.NoError(t, err, "Error creating ClusterRoleBinding: %s", string(output)) |
60 | | - |
61 | | - defer func() { |
62 | | - t.Log("Cleaning up ClusterRoleBinding") |
63 | | - _ = exec.Command(client, "delete", "clusterrolebinding", "operator-controller-metrics-binding", "--ignore-not-found=true").Run() |
64 | | - }() |
65 | | - |
66 | | - t.Log("Generating ServiceAccount token") |
67 | | - tokenCmd := exec.Command(client, "create", "token", "operator-controller-controller-manager", "-n", namespace) |
68 | | - tokenOutput, tokenCombinedOutput, err := stdoutAndCombined(tokenCmd) |
69 | | - require.NoError(t, err, "Error creating token: %s", string(tokenCombinedOutput)) |
70 | | - token = string(bytes.TrimSpace(tokenOutput)) |
71 | | - |
72 | | - t.Log("Creating curl pod to validate the metrics endpoint") |
73 | | - cmd = exec.Command(client, "run", curlPod, |
74 | | - "--image=curlimages/curl:7.87.0", "-n", namespace, |
| 105 | + require.NoError(c.t, err, "Error creating ClusterRoleBinding: %s", string(output)) |
| 106 | +} |
| 107 | + |
| 108 | +// getServiceAccountToken return the token requires to have access to the metrics |
| 109 | +func (c *MetricsTestConfig) getServiceAccountToken() string { |
| 110 | + c.t.Logf("Generating ServiceAccount token at namespace %s", c.namespace) |
| 111 | + cmd := exec.Command(c.client, "create", "token", c.serviceAccount, "-n", c.namespace) |
| 112 | + tokenOutput, tokenCombinedOutput, err := stdoutAndCombined(cmd) |
| 113 | + require.NoError(c.t, err, "Error creating token: %s", string(tokenCombinedOutput)) |
| 114 | + return string(bytes.TrimSpace(tokenOutput)) |
| 115 | +} |
| 116 | + |
| 117 | +// createCurlMetricsPod creates the Pod with curl image to allow check if the metrics are working |
| 118 | +func (c *MetricsTestConfig) createCurlMetricsPod() { |
| 119 | + c.t.Logf("Creating curl pod (%s/%s) to validate the metrics endpoint", c.namespace, c.curlPodName) |
| 120 | + cmd := exec.Command(c.client, "run", c.curlPodName, |
| 121 | + "--image=curlimages/curl", "-n", c.namespace, |
75 | 122 | "--restart=Never", |
76 | 123 | "--overrides", `{ |
77 | 124 | "spec": { |
78 | 125 | "containers": [{ |
79 | 126 | "name": "curl", |
80 | | - "image": "curlimages/curl:7.87.0", |
| 127 | + "image": "curlimages/curl", |
81 | 128 | "command": ["sh", "-c", "sleep 3600"], |
82 | 129 | "securityContext": { |
83 | 130 | "allowPrivilegeEscalation": false, |
84 | | - "capabilities": { |
85 | | - "drop": ["ALL"] |
86 | | - }, |
| 131 | + "capabilities": {"drop": ["ALL"]}, |
87 | 132 | "runAsNonRoot": true, |
88 | 133 | "runAsUser": 1000, |
89 | | - "seccompProfile": { |
90 | | - "type": "RuntimeDefault" |
91 | | - } |
| 134 | + "seccompProfile": {"type": "RuntimeDefault"} |
92 | 135 | } |
93 | 136 | }], |
94 | | - "serviceAccountName": "operator-controller-controller-manager" |
| 137 | + "serviceAccountName": "`+c.serviceAccount+`" |
95 | 138 | } |
96 | 139 | }`) |
97 | | - output, err = cmd.CombinedOutput() |
98 | | - require.NoError(t, err, "Error creating curl pod: %s", string(output)) |
99 | | - |
100 | | - defer func() { |
101 | | - t.Log("Cleaning up curl pod") |
102 | | - _ = exec.Command(client, "delete", "pod", curlPod, "-n", namespace, "--ignore-not-found=true").Run() |
103 | | - }() |
| 140 | + output, err := cmd.CombinedOutput() |
| 141 | + require.NoError(c.t, err, "Error creating curl pod: %s", string(output)) |
| 142 | +} |
104 | 143 |
|
105 | | - t.Log("Waiting for the curl pod to be ready") |
106 | | - waitCmd := exec.Command(client, "wait", "--for=condition=Ready", "pod", curlPod, "-n", namespace, "--timeout=60s") |
| 144 | +// validate verifies if is possible to access the metrics |
| 145 | +func (c *MetricsTestConfig) validate(token string) { |
| 146 | + c.t.Log("Waiting for the curl pod to be ready") |
| 147 | + waitCmd := exec.Command(c.client, "wait", "--for=condition=Ready", "pod", c.curlPodName, "-n", c.namespace, "--timeout=60s") |
107 | 148 | waitOutput, waitErr := waitCmd.CombinedOutput() |
108 | | - require.NoError(t, waitErr, "Error waiting for curl pod to be ready: %s", string(waitOutput)) |
109 | | - |
110 | | - t.Log("Validating the metrics endpoint") |
111 | | - metricsURL := "https://operator-controller-service." + namespace + ".svc.cluster.local:8443/metrics" |
112 | | - curlCmd := exec.Command(client, "exec", curlPod, "-n", namespace, "--", |
113 | | - "curl", "-v", "-k", "-H", "Authorization: Bearer "+token, metricsURL) |
114 | | - output, err = curlCmd.CombinedOutput() |
115 | | - require.NoError(t, err, "Error calling metrics endpoint: %s", string(output)) |
116 | | - require.Contains(t, string(output), "200 OK", "Metrics endpoint did not return 200 OK") |
| 149 | + require.NoError(c.t, waitErr, "Error waiting for curl pod to be ready: %s", string(waitOutput)) |
| 150 | + |
| 151 | + c.t.Log("Validating the metrics endpoint") |
| 152 | + curlCmd := exec.Command(c.client, "exec", c.curlPodName, "-n", c.namespace, "--", |
| 153 | + "curl", "-v", "-k", "-H", "Authorization: Bearer "+token, c.metricsURL) |
| 154 | + output, err := curlCmd.CombinedOutput() |
| 155 | + require.NoError(c.t, err, "Error calling metrics endpoint: %s", string(output)) |
| 156 | + require.Contains(c.t, string(output), "200 OK", "Metrics endpoint did not return 200 OK") |
117 | 157 | } |
118 | 158 |
|
119 | | -// nolint:gosec |
120 | | -// TestCatalogdMetricsExportedEndpoint verifies that the metrics endpoint for the catalogd |
121 | | -// is exported correctly and accessible by authorized users through RBAC and a ServiceAccount token. |
122 | | -// The test performs the following steps: |
123 | | -// 1. Creates a ClusterRoleBinding to grant necessary permissions for accessing metrics. |
124 | | -// 2. Generates a ServiceAccount token for authentication. |
125 | | -// 3. Deploys a curl pod to interact with the metrics endpoint. |
126 | | -// 4. Waits for the curl pod to become ready. |
127 | | -// 5. Executes a curl command from the pod to validate the metrics endpoint. |
128 | | -// 6. Cleans up all resources created during the test, such as the ClusterRoleBinding and curl pod. |
129 | | -func TestCatalogdMetricsExportedEndpoint(t *testing.T) { |
130 | | - var ( |
131 | | - token string |
132 | | - curlPod = "curl-metrics" |
133 | | - client = "" |
134 | | - clients = []string{"kubectl", "oc"} |
135 | | - ) |
136 | | - |
137 | | - t.Log("Looking for k8s client") |
138 | | - for _, c := range clients { |
139 | | - // Would prefer to use `command -v`, but even that may not be installed! |
140 | | - err := exec.Command(c, "version", "--client").Run() |
141 | | - if err == nil { |
142 | | - client = c |
143 | | - break |
144 | | - } |
145 | | - } |
146 | | - if client == "" { |
147 | | - t.Fatal("k8s client not found") |
148 | | - } |
149 | | - t.Logf("Using %q as k8s client", client) |
| 159 | +// cleanup created resources |
| 160 | +func (c *MetricsTestConfig) cleanup() { |
| 161 | + c.t.Log("Cleaning up resources") |
| 162 | + _ = exec.Command(c.client, "delete", "clusterrolebinding", c.clusterBinding, "--ignore-not-found=true").Run() |
| 163 | + _ = exec.Command(c.client, "delete", "pod", c.curlPodName, "-n", c.namespace, "--ignore-not-found=true").Run() |
| 164 | +} |
150 | 165 |
|
151 | | - t.Log("Determining catalogd namespace") |
152 | | - cmd := exec.Command(client, "get", "pods", "--all-namespaces", "--selector=control-plane=catalogd-controller-manager", "--output=jsonpath={.items[0].metadata.namespace}") |
| 166 | +// getComponentNamespace returns the namespace where operator-controller or catalogd is running |
| 167 | +func getComponentNamespace(t *testing.T, client, selector string) string { |
| 168 | + cmd := exec.Command(client, "get", "pods", "--all-namespaces", "--selector="+selector, "--output=jsonpath={.items[0].metadata.namespace}") |
153 | 169 | output, err := cmd.CombinedOutput() |
154 | | - require.NoError(t, err, "Error creating determining catalogd namespace: %s", string(output)) |
155 | | - namespace := string(output) |
| 170 | + require.NoError(t, err, "Error determining namespace: %s", string(output)) |
| 171 | + |
| 172 | + namespace := string(bytes.TrimSpace(output)) |
156 | 173 | if namespace == "" { |
157 | | - t.Fatal("No catalogd namespace found") |
| 174 | + t.Fatal("No namespace found for selector " + selector) |
158 | 175 | } |
159 | | - t.Logf("Using %q as catalogd namespace", namespace) |
160 | | - |
161 | | - t.Log("Creating ClusterRoleBinding for metrics access") |
162 | | - cmd = exec.Command(client, "create", "clusterrolebinding", "catalogd-metrics-binding", |
163 | | - "--clusterrole=catalogd-metrics-reader", |
164 | | - "--serviceaccount="+namespace+":catalogd-controller-manager") |
165 | | - output, err = cmd.CombinedOutput() |
166 | | - require.NoError(t, err, "Error creating ClusterRoleBinding: %s", string(output)) |
167 | | - |
168 | | - defer func() { |
169 | | - t.Log("Cleaning up ClusterRoleBinding") |
170 | | - _ = exec.Command(client, "delete", "clusterrolebinding", "catalogd-metrics-binding", "--ignore-not-found=true").Run() |
171 | | - }() |
172 | | - |
173 | | - t.Log("Creating service account token for authentication") |
174 | | - tokenCmd := exec.Command(client, "create", "token", "catalogd-controller-manager", "-n", namespace) |
175 | | - tokenOutput, tokenCombinedOutput, err := stdoutAndCombined(tokenCmd) |
176 | | - require.NoError(t, err, "Error creating token: %s", string(tokenCombinedOutput)) |
177 | | - token = string(bytes.TrimSpace(tokenOutput)) |
178 | | - |
179 | | - t.Log("Creating a pod to run curl commands") |
180 | | - cmd = exec.Command(client, "run", curlPod, |
181 | | - "--image=curlimages/curl:7.87.0", "-n", namespace, |
182 | | - "--restart=Never", |
183 | | - "--overrides", `{ |
184 | | - "spec": { |
185 | | - "containers": [{ |
186 | | - "name": "curl", |
187 | | - "image": "curlimages/curl:7.87.0", |
188 | | - "command": ["sh", "-c", "sleep 3600"], |
189 | | - "securityContext": { |
190 | | - "allowPrivilegeEscalation": false, |
191 | | - "capabilities": { |
192 | | - "drop": ["ALL"] |
193 | | - }, |
194 | | - "runAsNonRoot": true, |
195 | | - "runAsUser": 1000, |
196 | | - "seccompProfile": { |
197 | | - "type": "RuntimeDefault" |
198 | | - } |
199 | | - } |
200 | | - }], |
201 | | - "serviceAccountName": "catalogd-controller-manager" |
202 | | - } |
203 | | - }`) |
204 | | - output, err = cmd.CombinedOutput() |
205 | | - require.NoError(t, err, "Error creating curl pod: %s", string(output)) |
206 | | - |
207 | | - defer func() { |
208 | | - t.Log("Cleaning up curl pod") |
209 | | - _ = exec.Command(client, "delete", "pod", curlPod, "-n", namespace, "--ignore-not-found=true").Run() |
210 | | - }() |
211 | | - |
212 | | - t.Log("Waiting for the curl pod to become ready") |
213 | | - waitCmd := exec.Command(client, "wait", "--for=condition=Ready", "pod", curlPod, "-n", namespace, "--timeout=60s") |
214 | | - waitOutput, waitErr := waitCmd.CombinedOutput() |
215 | | - require.NoError(t, waitErr, "Error waiting for curl pod to be ready: %s", string(waitOutput)) |
216 | | - |
217 | | - t.Log("Validating the metrics endpoint") |
218 | | - metricsURL := "https://catalogd-service." + namespace + ".svc.cluster.local:7443/metrics" |
219 | | - curlCmd := exec.Command(client, "exec", curlPod, "-n", namespace, "--", |
220 | | - "curl", "-v", "-k", "-H", "Authorization: Bearer "+token, metricsURL) |
221 | | - output, err = curlCmd.CombinedOutput() |
222 | | - require.NoError(t, err, "Error calling metrics endpoint: %s", string(output)) |
223 | | - require.Contains(t, string(output), "200 OK", "Metrics endpoint did not return 200 OK") |
| 176 | + return namespace |
224 | 177 | } |
225 | 178 |
|
226 | 179 | func stdoutAndCombined(cmd *exec.Cmd) ([]byte, []byte, error) { |
227 | | - var outOnly bytes.Buffer |
228 | | - var outAndErr bytes.Buffer |
| 180 | + var outOnly, outAndErr bytes.Buffer |
229 | 181 | allWriter := io.MultiWriter(&outOnly, &outAndErr) |
230 | | - cmd.Stderr = &outAndErr |
231 | 182 | cmd.Stdout = allWriter |
| 183 | + cmd.Stderr = &outAndErr |
232 | 184 | err := cmd.Run() |
233 | 185 | return outOnly.Bytes(), outAndErr.Bytes(), err |
234 | 186 | } |
0 commit comments