Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions test/extended/node/image_volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ package node

import (
"context"
"fmt"
"regexp"
"strconv"
"strings"
"time"

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

g.It("should report kubelet image volume metrics correctly [OCP-84149]", func(ctx context.Context) {
const (
podName = "image-volume-metrics-test"
imageRef = "quay.io/crio/artifact:v1"
mountPath = "/mnt/image"
)

// Step 1: Create a pod with OCI image as volume source
g.By("Creating a pod with OCI image as volume source")
pod := buildPodWithImageVolume(f.Namespace.Name, "", podName, imageRef)
pod = createPodAndWaitForRunning(ctx, oc, pod)

// Step 2: Verify the image is mounted successfully and read-only
g.By("Verifying image volume is mounted into the container")
verifyImageVolumeMounted(f, pod, mountPath)

g.By("Verifying the mounted volume is read-only")
verifyVolumeReadOnly(f, pod, mountPath)

// Step 3: Check kubelet metrics about image volume
g.By("Checking kubelet metrics for image volume")
metrics, err := getKubeletMetrics(ctx, oc, pod.Spec.NodeName)
o.Expect(err).NotTo(o.HaveOccurred(), "Failed to get kubelet metrics")

g.By("Verifying kubelet_image_volume_requested_total metric")
requestedTotal, found := parseMetricValue(metrics, "kubelet_image_volume_requested_total")
o.Expect(found).To(o.BeTrue(), "kubelet_image_volume_requested_total metric should exist")
o.Expect(requestedTotal).To(o.BeNumerically(">=", 1),
"kubelet_image_volume_requested_total should be at least 1")
Comment on lines +121 to +122
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we test for an exact count here? Same for the 2 other cases below.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it intends to check if there's requested_total_count exists and is calculated correctly, not to check the exact count.


g.By("Verifying kubelet_image_volume_mounted_succeed_total metric")
succeededTotal, found := parseMetricValue(metrics, "kubelet_image_volume_mounted_succeed_total")
o.Expect(found).To(o.BeTrue(), "kubelet_image_volume_mounted_succeed_total metric should exist")
o.Expect(succeededTotal).To(o.BeNumerically(">=", 1),
"kubelet_image_volume_mounted_succeed_total should be at least 1")

g.By("Verifying kubelet_image_volume_mounted_errors_total metric")
errorsTotal, found := parseMetricValue(metrics, "kubelet_image_volume_mounted_errors_total")
o.Expect(found).To(o.BeTrue(), "kubelet_image_volume_mounted_errors_total metric should exist")
o.Expect(errorsTotal).To(o.Equal(0),
"kubelet_image_volume_mounted_errors_total should be 0")
})

g.Context("when subPath is used", func() {
g.It("should handle image volume with subPath", func(ctx context.Context) {
pod := buildPodWithImageVolumeSubPath(f.Namespace.Name, "", podName, image, "bin")
Expand Down Expand Up @@ -186,3 +233,80 @@ func buildPodWithMultipleImageVolumes(namespace, nodeName, podName, image1, imag
})
return pod
}

// verifyImageVolumeMounted verifies that the image volume is mounted and accessible
func verifyImageVolumeMounted(f *framework.Framework, pod *v1.Pod, mountPath string) {
g.By(fmt.Sprintf("Checking if volume is mounted at %s", mountPath))

// Verify the content of the expected file
stdout := e2epod.ExecCommandInContainer(f, pod.Name, pod.Spec.Containers[0].Name,
"cat", mountPath+"/file")
o.Expect(stdout).To(o.Equal("2"), "File content should be '2'")
}

// verifyVolumeReadOnly verifies that the mounted volume is read-only
func verifyVolumeReadOnly(f *framework.Framework, pod *v1.Pod, mountPath string) {
g.By("Verifying the volume is mounted as read-only")

// Check mount options
stdout := e2epod.ExecCommandInContainer(f, pod.Name, pod.Spec.Containers[0].Name,
"mount")
o.Expect(stdout).To(o.ContainSubstring(mountPath), "Mount point should be listed")

// Verify read-only in mount output
mountLines := strings.Split(stdout, "\n")
for _, line := range mountLines {
if strings.Contains(line, mountPath) {
o.Expect(line).To(o.MatchRegexp(`\bro\b`),
"Volume should be mounted with 'ro' (read-only) option")
framework.Logf("Mount info: %s", line)
break
}
}

// Try to write to the volume (should fail)
g.By("Attempting to write to the read-only volume (should fail)")
_, _, err := e2epod.ExecCommandInContainerWithFullOutput(f, pod.Name, pod.Spec.Containers[0].Name,
"touch", mountPath+"/testfile")
o.Expect(err).To(o.HaveOccurred(), "Writing to read-only volume should fail")
}

// getKubeletMetrics fetches kubelet metrics from a specific node
func getKubeletMetrics(ctx context.Context, oc *exutil.CLI, nodeName string) (string, error) {
metricsPath := fmt.Sprintf("/api/v1/nodes/%s/proxy/metrics", nodeName)

data, err := oc.AdminKubeClient().CoreV1().RESTClient().Get().
AbsPath(metricsPath).
DoRaw(ctx)
if err != nil {
return "", fmt.Errorf("failed to get metrics from node %s: %w", nodeName, err)
}

return string(data), nil
}

// parseMetricValue parses a Prometheus metric value from metrics output
func parseMetricValue(metrics, metricName string) (int, bool) {
// Look for lines like: kubelet_image_volume_requested_total 1
// Skip HELP and TYPE lines
re := regexp.MustCompile(fmt.Sprintf(`^%s\s+(\d+)`, metricName))

lines := strings.Split(metrics, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "#") {
continue // Skip comment lines
}

matches := re.FindStringSubmatch(line)
if len(matches) == 2 {
value, err := strconv.Atoi(matches[1])
if err == nil {
return value, true
}
}
}

framework.Logf("Metric %s not found in output", metricName)
return 0, false
}