k8s.io/kubernetes@v1.29.3/pkg/kubelet/server/stats/volume_stat_calculator_test.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package stats 18 19 import ( 20 "errors" 21 "fmt" 22 "testing" 23 "time" 24 25 "github.com/golang/mock/gomock" 26 "github.com/stretchr/testify/assert" 27 28 csipbv1 "github.com/container-storage-interface/spec/lib/go/csi" 29 k8sv1 "k8s.io/api/core/v1" 30 "k8s.io/apimachinery/pkg/api/resource" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 utilfeature "k8s.io/apiserver/pkg/util/feature" 33 "k8s.io/client-go/tools/record" 34 featuregatetesting "k8s.io/component-base/featuregate/testing" 35 kubestats "k8s.io/kubelet/pkg/apis/stats/v1alpha1" 36 "k8s.io/kubernetes/pkg/features" 37 statstest "k8s.io/kubernetes/pkg/kubelet/server/stats/testing" 38 "k8s.io/kubernetes/pkg/volume" 39 ) 40 41 const ( 42 namespace0 = "test0" 43 pName0 = "pod0" 44 capacity = int64(10000000) 45 available = int64(5000000) 46 inodesTotal = int64(2000) 47 inodesFree = int64(1000) 48 49 vol0 = "vol0" 50 vol1 = "vol1" 51 vol2 = "vol2" 52 vol3 = "vol3" 53 pvcClaimName0 = "pvc-fake0" 54 pvcClaimName1 = "pvc-fake1" 55 ) 56 57 var ( 58 ErrorWatchTimeout = errors.New("watch event timeout") 59 // Create pod spec to test against 60 podVolumes = []k8sv1.Volume{ 61 { 62 Name: vol0, 63 VolumeSource: k8sv1.VolumeSource{ 64 GCEPersistentDisk: &k8sv1.GCEPersistentDiskVolumeSource{ 65 PDName: "fake-device1", 66 }, 67 }, 68 }, 69 { 70 Name: vol1, 71 VolumeSource: k8sv1.VolumeSource{ 72 PersistentVolumeClaim: &k8sv1.PersistentVolumeClaimVolumeSource{ 73 ClaimName: pvcClaimName0, 74 }, 75 }, 76 }, 77 { 78 Name: vol2, 79 VolumeSource: k8sv1.VolumeSource{ 80 PersistentVolumeClaim: &k8sv1.PersistentVolumeClaimVolumeSource{ 81 ClaimName: pvcClaimName1, 82 }, 83 }, 84 }, 85 { 86 Name: vol3, 87 VolumeSource: k8sv1.VolumeSource{ 88 Ephemeral: &k8sv1.EphemeralVolumeSource{}, 89 }, 90 }, 91 } 92 93 fakePod = &k8sv1.Pod{ 94 ObjectMeta: metav1.ObjectMeta{ 95 Name: pName0, 96 Namespace: namespace0, 97 UID: "UID" + pName0, 98 }, 99 Spec: k8sv1.PodSpec{ 100 Volumes: podVolumes, 101 }, 102 } 103 104 volumeCondition = &csipbv1.VolumeCondition{} 105 ) 106 107 func TestPVCRef(t *testing.T) { 108 mockCtrl := gomock.NewController(t) 109 defer mockCtrl.Finish() 110 111 // Setup mock stats provider 112 mockStats := statstest.NewMockProvider(mockCtrl) 113 volumes := map[string]volume.Volume{vol0: &fakeVolume{}, vol1: &fakeVolume{}, vol3: &fakeVolume{}} 114 mockStats.EXPECT().ListVolumesForPod(fakePod.UID).Return(volumes, true) 115 blockVolumes := map[string]volume.BlockVolume{vol2: &fakeBlockVolume{}} 116 mockStats.EXPECT().ListBlockVolumesForPod(fakePod.UID).Return(blockVolumes, true) 117 118 eventStore := make(chan string, 1) 119 fakeEventRecorder := record.FakeRecorder{ 120 Events: eventStore, 121 } 122 123 // Calculate stats for pod 124 statsCalculator := newVolumeStatCalculator(mockStats, time.Minute, fakePod, &fakeEventRecorder) 125 statsCalculator.calcAndStoreStats() 126 vs, _ := statsCalculator.GetLatest() 127 128 assert.Len(t, append(vs.EphemeralVolumes, vs.PersistentVolumes...), 4) 129 // Verify 'vol0' doesn't have a PVC reference 130 assert.Contains(t, append(vs.EphemeralVolumes, vs.PersistentVolumes...), kubestats.VolumeStats{ 131 Name: vol0, 132 FsStats: expectedFSStats(), 133 VolumeHealthStats: expectedVolumeHealthStats(), 134 }) 135 // Verify 'vol1' has a PVC reference 136 assert.Contains(t, append(vs.EphemeralVolumes, vs.PersistentVolumes...), kubestats.VolumeStats{ 137 Name: vol1, 138 PVCRef: &kubestats.PVCReference{ 139 Name: pvcClaimName0, 140 Namespace: namespace0, 141 }, 142 FsStats: expectedFSStats(), 143 VolumeHealthStats: expectedVolumeHealthStats(), 144 }) 145 // // Verify 'vol2' has a PVC reference 146 assert.Contains(t, append(vs.EphemeralVolumes, vs.PersistentVolumes...), kubestats.VolumeStats{ 147 Name: vol2, 148 PVCRef: &kubestats.PVCReference{ 149 Name: pvcClaimName1, 150 Namespace: namespace0, 151 }, 152 FsStats: expectedBlockStats(), 153 VolumeHealthStats: expectedVolumeHealthStats(), 154 }) 155 // Verify 'vol3' has a PVC reference 156 assert.Contains(t, append(vs.EphemeralVolumes, vs.PersistentVolumes...), kubestats.VolumeStats{ 157 Name: vol3, 158 PVCRef: &kubestats.PVCReference{ 159 Name: pName0 + "-" + vol3, 160 Namespace: namespace0, 161 }, 162 FsStats: expectedFSStats(), 163 VolumeHealthStats: expectedVolumeHealthStats(), 164 }) 165 } 166 167 func TestNormalVolumeEvent(t *testing.T) { 168 mockCtrl := gomock.NewController(t) 169 defer mockCtrl.Finish() 170 mockStats := statstest.NewMockProvider(mockCtrl) 171 172 volumes := map[string]volume.Volume{vol0: &fakeVolume{}, vol1: &fakeVolume{}} 173 mockStats.EXPECT().ListVolumesForPod(fakePod.UID).Return(volumes, true) 174 blockVolumes := map[string]volume.BlockVolume{vol2: &fakeBlockVolume{}} 175 mockStats.EXPECT().ListBlockVolumesForPod(fakePod.UID).Return(blockVolumes, true) 176 177 eventStore := make(chan string, 2) 178 fakeEventRecorder := record.FakeRecorder{ 179 Events: eventStore, 180 } 181 182 // Calculate stats for pod 183 statsCalculator := newVolumeStatCalculator(mockStats, time.Minute, fakePod, &fakeEventRecorder) 184 statsCalculator.calcAndStoreStats() 185 186 event, err := WatchEvent(eventStore) 187 assert.NotNil(t, err) 188 assert.Equal(t, "", event) 189 } 190 191 func TestAbnormalVolumeEvent(t *testing.T) { 192 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CSIVolumeHealth, true)() 193 mockCtrl := gomock.NewController(t) 194 defer mockCtrl.Finish() 195 196 // Setup mock stats provider 197 mockStats := statstest.NewMockProvider(mockCtrl) 198 volumes := map[string]volume.Volume{vol0: &fakeVolume{}} 199 mockStats.EXPECT().ListVolumesForPod(fakePod.UID).Return(volumes, true) 200 blockVolumes := map[string]volume.BlockVolume{vol1: &fakeBlockVolume{}} 201 mockStats.EXPECT().ListBlockVolumesForPod(fakePod.UID).Return(blockVolumes, true) 202 203 eventStore := make(chan string, 2) 204 fakeEventRecorder := record.FakeRecorder{ 205 Events: eventStore, 206 } 207 208 // Calculate stats for pod 209 if volumeCondition != nil { 210 volumeCondition.Message = "The target path of the volume doesn't exist" 211 volumeCondition.Abnormal = true 212 } 213 statsCalculator := newVolumeStatCalculator(mockStats, time.Minute, fakePod, &fakeEventRecorder) 214 statsCalculator.calcAndStoreStats() 215 216 event, err := WatchEvent(eventStore) 217 assert.Nil(t, err) 218 assert.Equal(t, fmt.Sprintf("Warning VolumeConditionAbnormal Volume %s: The target path of the volume doesn't exist", "vol0"), event) 219 } 220 221 func WatchEvent(eventChan <-chan string) (string, error) { 222 select { 223 case event := <-eventChan: 224 return event, nil 225 case <-time.After(5 * time.Second): 226 return "", ErrorWatchTimeout 227 } 228 } 229 230 // Fake volume/metrics provider 231 var _ volume.Volume = &fakeVolume{} 232 233 type fakeVolume struct{} 234 235 func (v *fakeVolume) GetPath() string { return "" } 236 237 func (v *fakeVolume) GetMetrics() (*volume.Metrics, error) { 238 return expectedMetrics(), nil 239 } 240 241 func expectedMetrics() *volume.Metrics { 242 vMetrics := &volume.Metrics{ 243 Available: resource.NewQuantity(available, resource.BinarySI), 244 Capacity: resource.NewQuantity(capacity, resource.BinarySI), 245 Used: resource.NewQuantity(available-capacity, resource.BinarySI), 246 Inodes: resource.NewQuantity(inodesTotal, resource.BinarySI), 247 InodesFree: resource.NewQuantity(inodesFree, resource.BinarySI), 248 InodesUsed: resource.NewQuantity(inodesTotal-inodesFree, resource.BinarySI), 249 } 250 251 if volumeCondition != nil { 252 vMetrics.Message = &volumeCondition.Message 253 vMetrics.Abnormal = &volumeCondition.Abnormal 254 } 255 256 return vMetrics 257 } 258 259 func expectedFSStats() kubestats.FsStats { 260 metric := expectedMetrics() 261 available := uint64(metric.Available.Value()) 262 capacity := uint64(metric.Capacity.Value()) 263 used := uint64(metric.Used.Value()) 264 inodes := uint64(metric.Inodes.Value()) 265 inodesFree := uint64(metric.InodesFree.Value()) 266 inodesUsed := uint64(metric.InodesUsed.Value()) 267 return kubestats.FsStats{ 268 AvailableBytes: &available, 269 CapacityBytes: &capacity, 270 UsedBytes: &used, 271 Inodes: &inodes, 272 InodesFree: &inodesFree, 273 InodesUsed: &inodesUsed, 274 } 275 } 276 277 func expectedVolumeHealthStats() *kubestats.VolumeHealthStats { 278 metric := expectedMetrics() 279 hs := &kubestats.VolumeHealthStats{} 280 281 if metric != nil && metric.Abnormal != nil { 282 hs.Abnormal = *metric.Abnormal 283 } 284 285 return hs 286 } 287 288 // Fake block-volume/metrics provider, block-devices have no inodes 289 var _ volume.BlockVolume = &fakeBlockVolume{} 290 291 type fakeBlockVolume struct{} 292 293 func (v *fakeBlockVolume) GetGlobalMapPath(*volume.Spec) (string, error) { return "", nil } 294 295 func (v *fakeBlockVolume) GetPodDeviceMapPath() (string, string) { return "", "" } 296 297 func (v *fakeBlockVolume) SupportsMetrics() bool { return true } 298 299 func (v *fakeBlockVolume) GetMetrics() (*volume.Metrics, error) { 300 return expectedBlockMetrics(), nil 301 } 302 303 func expectedBlockMetrics() *volume.Metrics { 304 vMetrics := &volume.Metrics{ 305 Available: resource.NewQuantity(available, resource.BinarySI), 306 Capacity: resource.NewQuantity(capacity, resource.BinarySI), 307 Used: resource.NewQuantity(available-capacity, resource.BinarySI), 308 } 309 310 if volumeCondition != nil { 311 vMetrics.Abnormal = &volumeCondition.Abnormal 312 } 313 314 return vMetrics 315 } 316 317 func expectedBlockStats() kubestats.FsStats { 318 metric := expectedBlockMetrics() 319 available := uint64(metric.Available.Value()) 320 capacity := uint64(metric.Capacity.Value()) 321 used := uint64(metric.Used.Value()) 322 null := uint64(0) 323 return kubestats.FsStats{ 324 AvailableBytes: &available, 325 CapacityBytes: &capacity, 326 UsedBytes: &used, 327 Inodes: &null, 328 InodesFree: &null, 329 InodesUsed: &null, 330 } 331 }