Skip to content

Commit eeab3da

Browse files
Merge pull request #30657 from lyman9966/OCP-84149
OCPNODE-3944: Create e2e automation in origin for case OCP-84149
2 parents 2b2156b + bbc93b0 commit eeab3da

1 file changed

Lines changed: 126 additions & 0 deletions

File tree

test/extended/node/image_volume.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ package node
22

33
import (
44
"context"
5+
"fmt"
6+
"regexp"
7+
"strconv"
8+
"strings"
59
"time"
610

711
g "github.com/onsi/ginkgo/v2"
@@ -87,6 +91,49 @@ var _ = g.Describe("[sig-node] [FeatureGate:ImageVolume] ImageVolume", func() {
8791
verifyVolumeMounted(f, pod2, "ls", "/mnt/image/bin/oc")
8892
})
8993

94+
g.It("should report kubelet image volume metrics correctly [OCP-84149]", func(ctx context.Context) {
95+
const (
96+
podName = "image-volume-metrics-test"
97+
imageRef = "image-registry.openshift-image-registry.svc:5000/openshift/cli:latest"
98+
mountPath = "/mnt/image"
99+
)
100+
101+
// Step 1: Create a pod with OCI image as volume source
102+
g.By("Creating a pod with OCI image as volume source")
103+
pod := buildPodWithImageVolume(f.Namespace.Name, "", podName, imageRef)
104+
pod = createPodAndWaitForRunning(ctx, oc, pod)
105+
106+
// Step 2: Verify the image is mounted successfully and read-only
107+
g.By("Verifying image volume is mounted into the container")
108+
verifyImageVolumeMounted(f, pod, mountPath)
109+
110+
g.By("Verifying the mounted volume is read-only")
111+
verifyVolumeReadOnly(f, pod, mountPath)
112+
113+
// Step 3: Check kubelet metrics about image volume
114+
g.By("Checking kubelet metrics for image volume")
115+
metrics, err := getKubeletMetrics(ctx, oc, pod.Spec.NodeName)
116+
o.Expect(err).NotTo(o.HaveOccurred(), "Failed to get kubelet metrics")
117+
118+
g.By("Verifying kubelet_image_volume_requested_total metric")
119+
requestedTotal, found := parseMetricValue(metrics, "kubelet_image_volume_requested_total")
120+
o.Expect(found).To(o.BeTrue(), "kubelet_image_volume_requested_total metric should exist")
121+
o.Expect(requestedTotal).To(o.BeNumerically(">=", 1),
122+
"kubelet_image_volume_requested_total should be at least 1")
123+
124+
g.By("Verifying kubelet_image_volume_mounted_succeed_total metric")
125+
succeededTotal, found := parseMetricValue(metrics, "kubelet_image_volume_mounted_succeed_total")
126+
o.Expect(found).To(o.BeTrue(), "kubelet_image_volume_mounted_succeed_total metric should exist")
127+
o.Expect(succeededTotal).To(o.BeNumerically(">=", 1),
128+
"kubelet_image_volume_mounted_succeed_total should be at least 1")
129+
130+
g.By("Verifying kubelet_image_volume_mounted_errors_total metric")
131+
errorsTotal, found := parseMetricValue(metrics, "kubelet_image_volume_mounted_errors_total")
132+
o.Expect(found).To(o.BeTrue(), "kubelet_image_volume_mounted_errors_total metric should exist")
133+
o.Expect(errorsTotal).To(o.Equal(0),
134+
"kubelet_image_volume_mounted_errors_total should be 0")
135+
})
136+
90137
g.Context("when subPath is used", func() {
91138
g.It("should handle image volume with subPath", func(ctx context.Context) {
92139
pod := buildPodWithImageVolumeSubPath(f.Namespace.Name, "", podName, image, "bin")
@@ -186,3 +233,82 @@ func buildPodWithMultipleImageVolumes(namespace, nodeName, podName, image1, imag
186233
})
187234
return pod
188235
}
236+
237+
// verifyImageVolumeMounted verifies that the image volume is mounted and accessible
238+
func verifyImageVolumeMounted(f *framework.Framework, pod *v1.Pod, mountPath string) {
239+
g.By(fmt.Sprintf("Checking if volume is mounted at %s", mountPath))
240+
241+
// Verify the content of the expected file
242+
stdout := e2epod.ExecCommandInContainer(f, pod.Name, pod.Spec.Containers[0].Name,
243+
"cat", mountPath+"/etc/system-release")
244+
o.Expect(stdout).To(o.ContainSubstring("Red Hat Enterprise Linux release"), "File content should include 'Red Hat Enterprise Linux release'")
245+
}
246+
247+
// verifyVolumeReadOnly verifies that the mounted volume is read-only
248+
func verifyVolumeReadOnly(f *framework.Framework, pod *v1.Pod, mountPath string) {
249+
g.By("Verifying the volume is mounted as read-only")
250+
251+
// Check mount options
252+
stdout := e2epod.ExecCommandInContainer(f, pod.Name, pod.Spec.Containers[0].Name,
253+
"mount")
254+
o.Expect(stdout).To(o.ContainSubstring(mountPath), "Mount point should be listed")
255+
256+
// Verify read-only in mount output
257+
mountLines := strings.Split(stdout, "\n")
258+
for _, line := range mountLines {
259+
if strings.Contains(line, mountPath) {
260+
o.Expect(line).To(o.MatchRegexp(`\bro\b`),
261+
"Volume should be mounted with 'ro' (read-only) option")
262+
framework.Logf("Mount info: %s", line)
263+
break
264+
}
265+
}
266+
267+
// Try to write to the volume (should fail)
268+
g.By("Attempting to write to the read-only volume (should fail)")
269+
_, _, err := e2epod.ExecCommandInContainerWithFullOutput(f, pod.Name, pod.Spec.Containers[0].Name,
270+
"touch", mountPath+"/testfile")
271+
o.Expect(err).To(o.HaveOccurred(), "Writing to read-only volume should fail")
272+
}
273+
274+
// getKubeletMetrics fetches kubelet metrics from a specific node
275+
func getKubeletMetrics(ctx context.Context, oc *exutil.CLI, nodeName string) (string, error) {
276+
metricsPath := fmt.Sprintf("/api/v1/nodes/%s/proxy/metrics", nodeName)
277+
278+
data, err := oc.AdminKubeClient().CoreV1().RESTClient().Get().
279+
AbsPath(metricsPath).
280+
DoRaw(ctx)
281+
if err != nil {
282+
return "", fmt.Errorf("failed to get metrics from node %s: %w", nodeName, err)
283+
}
284+
285+
return string(data), nil
286+
}
287+
288+
// parseMetricValue parses a Prometheus metric value from metrics output
289+
// it returns (value, true) when it finds expected metricName, and value is x in example 'kubelet_image_volume_requested_total x'
290+
// it returns (0, false) when it can't find expected metricName
291+
func parseMetricValue(metrics, metricName string) (int, bool) {
292+
// Look for lines like: kubelet_image_volume_requested_total 1
293+
// Skip HELP and TYPE lines
294+
re := regexp.MustCompile(fmt.Sprintf(`^%s\s+(\d+)`, metricName))
295+
296+
lines := strings.Split(metrics, "\n")
297+
for _, line := range lines {
298+
line = strings.TrimSpace(line)
299+
if strings.HasPrefix(line, "#") {
300+
continue // Skip comment lines
301+
}
302+
303+
matches := re.FindStringSubmatch(line)
304+
if len(matches) == 2 {
305+
value, err := strconv.Atoi(matches[1])
306+
if err == nil {
307+
return value, true
308+
}
309+
}
310+
}
311+
312+
framework.Logf("Metric %s not found in output", metricName)
313+
return 0, false
314+
}

0 commit comments

Comments
 (0)