1
16
17 package e2enode
18
19 import (
20 "context"
21 "fmt"
22 "os"
23 "strings"
24 "time"
25
26 v1 "k8s.io/api/core/v1"
27 "k8s.io/apimachinery/pkg/api/resource"
28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29 kubeletstatsv1alpha1 "k8s.io/kubelet/pkg/apis/stats/v1alpha1"
30 "k8s.io/kubernetes/test/e2e/framework"
31 e2ekubectl "k8s.io/kubernetes/test/e2e/framework/kubectl"
32 e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
33 e2evolume "k8s.io/kubernetes/test/e2e/framework/volume"
34 admissionapi "k8s.io/pod-security-admission/api"
35
36 systemdutil "github.com/coreos/go-systemd/v22/util"
37 "github.com/onsi/ginkgo/v2"
38 "github.com/onsi/gomega"
39 "github.com/onsi/gomega/gstruct"
40 "github.com/onsi/gomega/types"
41 )
42
43 var _ = SIGDescribe("Summary API", framework.WithNodeConformance(), func() {
44 f := framework.NewDefaultFramework("summary-test")
45 f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged
46 ginkgo.Context("when querying /stats/summary", func() {
47 ginkgo.AfterEach(func(ctx context.Context) {
48 if !ginkgo.CurrentSpecReport().Failed() {
49 return
50 }
51 if framework.TestContext.DumpLogsOnFailure {
52 e2ekubectl.LogFailedContainers(ctx, f.ClientSet, f.Namespace.Name, framework.Logf)
53 }
54 ginkgo.By("Recording processes in system cgroups")
55 recordSystemCgroupProcesses(ctx)
56 })
57 ginkgo.It("should report resource usage through the stats api", func(ctx context.Context) {
58 const pod0 = "stats-busybox-0"
59 const pod1 = "stats-busybox-1"
60
61 ginkgo.By("Creating test pods")
62 numRestarts := int32(1)
63 pods := getSummaryTestPods(f, numRestarts, pod0, pod1)
64 e2epod.NewPodClient(f).CreateBatch(ctx, pods)
65
66 ginkgo.By("restarting the containers to ensure container metrics are still being gathered after a container is restarted")
67 gomega.Eventually(ctx, func() error {
68 for _, pod := range pods {
69 err := verifyPodRestartCount(ctx, f, pod.Name, len(pod.Spec.Containers), numRestarts)
70 if err != nil {
71 return err
72 }
73 }
74 return nil
75 }, time.Minute, 5*time.Second).Should(gomega.BeNil())
76
77 ginkgo.By("Waiting 15 seconds for cAdvisor to collect 2 stats points")
78 time.Sleep(15 * time.Second)
79
80
81 const (
82 maxStartAge = time.Hour * 24 * 365
83 maxStatsAge = time.Minute
84 )
85 ginkgo.By("Fetching node so we can match against an appropriate memory limit")
86 node := getLocalNode(ctx, f)
87 memoryCapacity := node.Status.Capacity["memory"]
88 memoryLimit := memoryCapacity.Value()
89 fsCapacityBounds := bounded(100*e2evolume.Mb, 10*e2evolume.Tb)
90
91 sysContExpectations := func() types.GomegaMatcher {
92 return gstruct.MatchAllFields(gstruct.Fields{
93 "Name": gstruct.Ignore(),
94 "StartTime": recent(maxStartAge),
95 "CPU": ptrMatchAllFields(gstruct.Fields{
96 "Time": recent(maxStatsAge),
97
98
99
100
101 "UsageNanoCores": gomega.SatisfyAny(gstruct.PointTo(gomega.BeZero()), bounded(10000, 2e9)),
102 "UsageCoreNanoSeconds": bounded(10000000, 1e15),
103 }),
104 "Memory": ptrMatchAllFields(gstruct.Fields{
105 "Time": recent(maxStatsAge),
106
107 "AvailableBytes": gomega.BeNil(),
108 "UsageBytes": bounded(1*e2evolume.Mb, memoryLimit),
109 "WorkingSetBytes": bounded(1*e2evolume.Mb, memoryLimit),
110
111 "RSSBytes": bounded(1*e2evolume.Mb, memoryLimit),
112 "PageFaults": bounded(1000, 1e9),
113 "MajorPageFaults": bounded(0, 1e9),
114 }),
115 "Swap": swapExpectation(memoryLimit),
116 "Accelerators": gomega.BeEmpty(),
117 "Rootfs": gomega.BeNil(),
118 "Logs": gomega.BeNil(),
119 "UserDefinedMetrics": gomega.BeEmpty(),
120 })
121 }
122 expectedPageFaultsUpperBound := 1000000
123 expectedMajorPageFaultsUpperBound := 1e9
124 if IsCgroup2UnifiedMode() {
125
126
127 expectedPageFaultsUpperBound = 1e9
128 expectedMajorPageFaultsUpperBound = 1e9
129 }
130
131 podsContExpectations := sysContExpectations().(*gstruct.FieldsMatcher)
132 podsContExpectations.Fields["Memory"] = ptrMatchAllFields(gstruct.Fields{
133 "Time": recent(maxStatsAge),
134
135 "AvailableBytes": bounded(1*e2evolume.Kb, memoryLimit),
136 "UsageBytes": bounded(10*e2evolume.Kb, memoryLimit),
137 "WorkingSetBytes": bounded(10*e2evolume.Kb, memoryLimit),
138 "RSSBytes": bounded(1*e2evolume.Kb, memoryLimit),
139 "PageFaults": bounded(0, expectedPageFaultsUpperBound),
140 "MajorPageFaults": bounded(0, expectedMajorPageFaultsUpperBound),
141 })
142 runtimeContExpectations := sysContExpectations().(*gstruct.FieldsMatcher)
143 systemContainers := gstruct.Elements{
144 "kubelet": sysContExpectations(),
145 "runtime": runtimeContExpectations,
146 "pods": podsContExpectations,
147 }
148
149 if !systemdutil.IsRunningSystemd() {
150 framework.Logf("Host not running systemd; expecting 'misc' system container.")
151 miscContExpectations := sysContExpectations().(*gstruct.FieldsMatcher)
152
153 miscContExpectations.Fields["Memory"] = ptrMatchAllFields(gstruct.Fields{
154 "Time": recent(maxStatsAge),
155
156 "AvailableBytes": gomega.BeNil(),
157 "UsageBytes": bounded(100*e2evolume.Kb, memoryLimit),
158 "WorkingSetBytes": bounded(100*e2evolume.Kb, memoryLimit),
159 "RSSBytes": bounded(100*e2evolume.Kb, memoryLimit),
160 "PageFaults": bounded(1000, 1e9),
161 "MajorPageFaults": bounded(0, 1e9),
162 })
163 systemContainers["misc"] = miscContExpectations
164 }
165
166 podExpectations := gstruct.MatchAllFields(gstruct.Fields{
167 "PodRef": gstruct.Ignore(),
168 "StartTime": recent(maxStartAge),
169 "Containers": gstruct.MatchAllElements(summaryObjectID, gstruct.Elements{
170 "busybox-container": gstruct.MatchAllFields(gstruct.Fields{
171 "Name": gomega.Equal("busybox-container"),
172 "StartTime": recent(maxStartAge),
173 "CPU": ptrMatchAllFields(gstruct.Fields{
174 "Time": recent(maxStatsAge),
175 "UsageNanoCores": bounded(10000, 1e9),
176 "UsageCoreNanoSeconds": bounded(10000000, 1e11),
177 }),
178 "Memory": ptrMatchAllFields(gstruct.Fields{
179 "Time": recent(maxStatsAge),
180 "AvailableBytes": bounded(1*e2evolume.Kb, 80*e2evolume.Mb),
181 "UsageBytes": bounded(10*e2evolume.Kb, 80*e2evolume.Mb),
182 "WorkingSetBytes": bounded(10*e2evolume.Kb, 80*e2evolume.Mb),
183 "RSSBytes": bounded(1*e2evolume.Kb, 80*e2evolume.Mb),
184 "PageFaults": bounded(100, expectedPageFaultsUpperBound),
185 "MajorPageFaults": bounded(0, expectedMajorPageFaultsUpperBound),
186 }),
187 "Swap": swapExpectation(memoryLimit),
188 "Accelerators": gomega.BeEmpty(),
189 "Rootfs": ptrMatchAllFields(gstruct.Fields{
190 "Time": recent(maxStatsAge),
191 "AvailableBytes": fsCapacityBounds,
192 "CapacityBytes": fsCapacityBounds,
193 "UsedBytes": bounded(e2evolume.Kb, 10*e2evolume.Mb),
194 "InodesFree": bounded(1e4, 1e8),
195 "Inodes": bounded(1e4, 1e8),
196 "InodesUsed": bounded(0, 1e8),
197 }),
198 "Logs": ptrMatchAllFields(gstruct.Fields{
199 "Time": recent(maxStatsAge),
200 "AvailableBytes": fsCapacityBounds,
201 "CapacityBytes": fsCapacityBounds,
202 "UsedBytes": bounded(e2evolume.Kb, 10*e2evolume.Mb),
203 "InodesFree": bounded(1e4, 1e8),
204 "Inodes": bounded(1e4, 1e8),
205 "InodesUsed": bounded(0, 1e8),
206 }),
207 "UserDefinedMetrics": gomega.BeEmpty(),
208 }),
209 }),
210 "Network": ptrMatchAllFields(gstruct.Fields{
211 "Time": recent(maxStatsAge),
212 "InterfaceStats": gstruct.MatchAllFields(gstruct.Fields{
213 "Name": gomega.Equal("eth0"),
214 "RxBytes": bounded(10, 10*e2evolume.Mb),
215 "RxErrors": bounded(0, 1000),
216 "TxBytes": bounded(10, 10*e2evolume.Mb),
217 "TxErrors": bounded(0, 1000),
218 }),
219 "Interfaces": gomega.Not(gomega.BeNil()),
220 }),
221 "CPU": ptrMatchAllFields(gstruct.Fields{
222 "Time": recent(maxStatsAge),
223 "UsageNanoCores": bounded(10000, 1e9),
224 "UsageCoreNanoSeconds": bounded(10000000, 1e11),
225 }),
226 "Memory": ptrMatchAllFields(gstruct.Fields{
227 "Time": recent(maxStatsAge),
228 "AvailableBytes": bounded(1*e2evolume.Kb, 80*e2evolume.Mb),
229 "UsageBytes": bounded(10*e2evolume.Kb, 80*e2evolume.Mb),
230 "WorkingSetBytes": bounded(10*e2evolume.Kb, 80*e2evolume.Mb),
231 "RSSBytes": bounded(1*e2evolume.Kb, 80*e2evolume.Mb),
232 "PageFaults": bounded(0, expectedPageFaultsUpperBound),
233 "MajorPageFaults": bounded(0, expectedMajorPageFaultsUpperBound),
234 }),
235 "Swap": swapExpectation(memoryLimit),
236 "VolumeStats": gstruct.MatchAllElements(summaryObjectID, gstruct.Elements{
237 "test-empty-dir": gstruct.MatchAllFields(gstruct.Fields{
238 "Name": gomega.Equal("test-empty-dir"),
239 "PVCRef": gomega.BeNil(),
240 "VolumeHealthStats": gomega.BeNil(),
241 "FsStats": gstruct.MatchAllFields(gstruct.Fields{
242 "Time": recent(maxStatsAge),
243 "AvailableBytes": fsCapacityBounds,
244 "CapacityBytes": fsCapacityBounds,
245 "UsedBytes": bounded(e2evolume.Kb, 1*e2evolume.Mb),
246 "InodesFree": bounded(1e4, 1e8),
247 "Inodes": bounded(1e4, 1e8),
248 "InodesUsed": bounded(0, 1e8),
249 }),
250 }),
251 }),
252 "EphemeralStorage": ptrMatchAllFields(gstruct.Fields{
253 "Time": recent(maxStatsAge),
254 "AvailableBytes": fsCapacityBounds,
255 "CapacityBytes": fsCapacityBounds,
256 "UsedBytes": bounded(e2evolume.Kb, 21*e2evolume.Mb),
257 "InodesFree": bounded(1e4, 1e8),
258 "Inodes": bounded(1e4, 1e8),
259 "InodesUsed": bounded(0, 1e8),
260 }),
261 "ProcessStats": ptrMatchAllFields(gstruct.Fields{
262 "ProcessCount": bounded(0, 1e8),
263 }),
264 })
265
266 matchExpectations := ptrMatchAllFields(gstruct.Fields{
267 "Node": gstruct.MatchAllFields(gstruct.Fields{
268 "NodeName": gomega.Equal(framework.TestContext.NodeName),
269 "StartTime": recent(maxStartAge),
270 "SystemContainers": gstruct.MatchAllElements(summaryObjectID, systemContainers),
271 "CPU": ptrMatchAllFields(gstruct.Fields{
272 "Time": recent(maxStatsAge),
273 "UsageNanoCores": bounded(100e3, 2e9),
274 "UsageCoreNanoSeconds": bounded(1e9, 1e15),
275 }),
276 "Memory": ptrMatchAllFields(gstruct.Fields{
277 "Time": recent(maxStatsAge),
278 "AvailableBytes": bounded(100*e2evolume.Mb, memoryLimit),
279 "UsageBytes": bounded(10*e2evolume.Mb, memoryLimit),
280 "WorkingSetBytes": bounded(10*e2evolume.Mb, memoryLimit),
281
282 "RSSBytes": bounded(1*e2evolume.Kb, memoryLimit),
283 "PageFaults": bounded(1000, 1e9),
284 "MajorPageFaults": bounded(0, 1e9),
285 }),
286 "Swap": swapExpectation(memoryLimit),
287
288 "Network": ptrMatchAllFields(gstruct.Fields{
289 "Time": recent(maxStatsAge),
290 "InterfaceStats": gstruct.MatchAllFields(gstruct.Fields{
291 "Name": gomega.Or(gomega.BeEmpty(), gomega.Equal("eth0")),
292 "RxBytes": gomega.Or(gomega.BeNil(), bounded(1*e2evolume.Mb, 100*e2evolume.Gb)),
293 "RxErrors": gomega.Or(gomega.BeNil(), bounded(0, 100000)),
294 "TxBytes": gomega.Or(gomega.BeNil(), bounded(10*e2evolume.Kb, 10*e2evolume.Gb)),
295 "TxErrors": gomega.Or(gomega.BeNil(), bounded(0, 100000)),
296 }),
297 "Interfaces": gomega.Not(gomega.BeNil()),
298 }),
299 "Fs": ptrMatchAllFields(gstruct.Fields{
300 "Time": recent(maxStatsAge),
301 "AvailableBytes": fsCapacityBounds,
302 "CapacityBytes": fsCapacityBounds,
303
304 "UsedBytes": bounded(e2evolume.Kb, 10*e2evolume.Tb),
305 "InodesFree": bounded(1e4, 1e8),
306 "Inodes": bounded(1e4, 1e8),
307 "InodesUsed": bounded(0, 1e8),
308 }),
309 "Runtime": ptrMatchAllFields(gstruct.Fields{
310 "ImageFs": ptrMatchAllFields(gstruct.Fields{
311 "Time": recent(maxStatsAge),
312 "AvailableBytes": fsCapacityBounds,
313 "CapacityBytes": fsCapacityBounds,
314
315 "UsedBytes": bounded(e2evolume.Kb, 10*e2evolume.Tb),
316 "InodesFree": bounded(1e4, 1e8),
317 "Inodes": bounded(1e4, 1e8),
318 "InodesUsed": bounded(0, 1e8),
319 }),
320 "ContainerFs": ptrMatchAllFields(gstruct.Fields{
321 "Time": recent(maxStatsAge),
322 "AvailableBytes": fsCapacityBounds,
323 "CapacityBytes": fsCapacityBounds,
324
325 "UsedBytes": bounded(e2evolume.Kb, 10*e2evolume.Tb),
326 "InodesFree": bounded(1e4, 1e8),
327 "Inodes": bounded(1e4, 1e8),
328 "InodesUsed": bounded(0, 1e8),
329 }),
330 }),
331 "Rlimit": ptrMatchAllFields(gstruct.Fields{
332 "Time": recent(maxStatsAge),
333 "MaxPID": bounded(0, 1e8),
334 "NumOfRunningProcesses": bounded(0, 1e8),
335 }),
336 }),
337
338 "Pods": gstruct.MatchElements(summaryObjectID, gstruct.IgnoreExtras, gstruct.Elements{
339 fmt.Sprintf("%s::%s", f.Namespace.Name, pod0): podExpectations,
340 fmt.Sprintf("%s::%s", f.Namespace.Name, pod1): podExpectations,
341 }),
342 })
343
344 ginkgo.By("Validating /stats/summary")
345
346 gomega.Eventually(ctx, getNodeSummary, 180*time.Second, 15*time.Second).Should(matchExpectations)
347
348 gomega.Consistently(ctx, getNodeSummary, 30*time.Second, 15*time.Second).Should(matchExpectations)
349 })
350 })
351 })
352
353 func getSummaryTestPods(f *framework.Framework, numRestarts int32, names ...string) []*v1.Pod {
354 pods := make([]*v1.Pod, 0, len(names))
355 for _, name := range names {
356 pods = append(pods, &v1.Pod{
357 ObjectMeta: metav1.ObjectMeta{
358 Name: name,
359 },
360 Spec: v1.PodSpec{
361 RestartPolicy: v1.RestartPolicyAlways,
362 Containers: []v1.Container{
363 {
364 Name: "busybox-container",
365 Image: busyboxImage,
366 SecurityContext: &v1.SecurityContext{
367 Capabilities: &v1.Capabilities{
368 Add: []v1.Capability{"NET_RAW"},
369 },
370 },
371 Command: getRestartingContainerCommand("/test-empty-dir-mnt", 0, numRestarts, "echo 'some bytes' >/outside_the_volume.txt; ping -c 1 google.com; echo 'hello world' >> /test-empty-dir-mnt/file;"),
372 Resources: v1.ResourceRequirements{
373 Limits: v1.ResourceList{
374
375 v1.ResourceMemory: resource.MustParse("80M"),
376 },
377 },
378 VolumeMounts: []v1.VolumeMount{
379 {MountPath: "/test-empty-dir-mnt", Name: "test-empty-dir"},
380 },
381 },
382 },
383 SecurityContext: &v1.PodSecurityContext{
384 SELinuxOptions: &v1.SELinuxOptions{
385 Level: "s0",
386 },
387 },
388 Volumes: []v1.Volume{
389
390
391 {Name: "test-empty-dir", VolumeSource: v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{}}},
392 },
393 },
394 })
395 }
396 return pods
397 }
398
399
400 func summaryObjectID(element interface{}) string {
401 switch el := element.(type) {
402 case kubeletstatsv1alpha1.PodStats:
403 return fmt.Sprintf("%s::%s", el.PodRef.Namespace, el.PodRef.Name)
404 case kubeletstatsv1alpha1.ContainerStats:
405 return el.Name
406 case kubeletstatsv1alpha1.VolumeStats:
407 return el.Name
408 case kubeletstatsv1alpha1.UserDefinedMetric:
409 return el.Name
410 default:
411 framework.Failf("Unknown type: %T", el)
412 return "???"
413 }
414 }
415
416
417 func ptrMatchAllFields(fields gstruct.Fields) types.GomegaMatcher {
418 return gstruct.PointTo(gstruct.MatchAllFields(fields))
419 }
420
421 func bounded(lower, upper interface{}) types.GomegaMatcher {
422 return gstruct.PointTo(gomega.And(
423 gomega.BeNumerically(">=", lower),
424 gomega.BeNumerically("<=", upper)))
425 }
426
427 func swapExpectation(upper interface{}) types.GomegaMatcher {
428
429
430 const maxMemorySize = uint64(1 << 62)
431
432 swapBytesMatcher := gomega.Or(
433 gomega.BeNil(),
434 bounded(0, upper),
435 gstruct.PointTo(gomega.BeNumerically(">=", maxMemorySize)),
436 )
437
438 return gomega.Or(
439 gomega.BeNil(),
440 ptrMatchAllFields(gstruct.Fields{
441 "Time": recent(maxStatsAge),
442 "SwapUsageBytes": swapBytesMatcher,
443 "SwapAvailableBytes": swapBytesMatcher,
444 }),
445 )
446 }
447
448 func recent(d time.Duration) types.GomegaMatcher {
449 return gomega.WithTransform(func(t metav1.Time) time.Time {
450 return t.Time
451 }, gomega.And(
452 gomega.BeTemporally(">=", time.Now().Add(-d)),
453
454 gomega.BeTemporally("<", time.Now().Add(3*time.Minute))))
455 }
456
457 func recordSystemCgroupProcesses(ctx context.Context) {
458 cfg, err := getCurrentKubeletConfig(ctx)
459 if err != nil {
460 framework.Logf("Failed to read kubelet config: %v", err)
461 return
462 }
463 cgroups := map[string]string{
464 "kubelet": cfg.KubeletCgroups,
465 "misc": cfg.SystemCgroups,
466 }
467 for name, cgroup := range cgroups {
468 if cgroup == "" {
469 framework.Logf("Skipping unconfigured cgroup %s", name)
470 continue
471 }
472
473 filePattern := "/sys/fs/cgroup/cpu/%s/cgroup.procs"
474 if IsCgroup2UnifiedMode() {
475 filePattern = "/sys/fs/cgroup/%s/cgroup.procs"
476 }
477 pids, err := os.ReadFile(fmt.Sprintf(filePattern, cgroup))
478 if err != nil {
479 framework.Logf("Failed to read processes in cgroup %s: %v", name, err)
480 continue
481 }
482
483 framework.Logf("Processes in %s cgroup (%s):", name, cgroup)
484 for _, pid := range strings.Fields(string(pids)) {
485 path := fmt.Sprintf("/proc/%s/cmdline", pid)
486 cmd, err := os.ReadFile(path)
487 if err != nil {
488 framework.Logf(" ginkgo.Failed to read %s: %v", path, err)
489 } else {
490 framework.Logf(" %s", cmd)
491 }
492 }
493 }
494 }
495
View as plain text