k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/kubelet/nodestatus/setters_test.go (about) 1 /* 2 Copyright 2018 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 nodestatus 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "net" 24 "sort" 25 "strconv" 26 "testing" 27 "time" 28 29 cadvisorapiv1 "github.com/google/cadvisor/info/v1" 30 "github.com/google/go-cmp/cmp" 31 "github.com/stretchr/testify/assert" 32 "github.com/stretchr/testify/require" 33 34 v1 "k8s.io/api/core/v1" 35 apiequality "k8s.io/apimachinery/pkg/api/equality" 36 "k8s.io/apimachinery/pkg/api/resource" 37 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 38 "k8s.io/apimachinery/pkg/util/rand" 39 "k8s.io/apimachinery/pkg/util/uuid" 40 utilfeature "k8s.io/apiserver/pkg/util/feature" 41 cloudprovider "k8s.io/cloud-provider" 42 fakecloud "k8s.io/cloud-provider/fake" 43 featuregatetesting "k8s.io/component-base/featuregate/testing" 44 "k8s.io/component-base/version" 45 "k8s.io/kubernetes/pkg/features" 46 "k8s.io/kubernetes/pkg/kubelet/cm" 47 kubecontainer "k8s.io/kubernetes/pkg/kubelet/container" 48 kubecontainertest "k8s.io/kubernetes/pkg/kubelet/container/testing" 49 "k8s.io/kubernetes/pkg/kubelet/events" 50 "k8s.io/kubernetes/pkg/kubelet/util/sliceutils" 51 netutils "k8s.io/utils/net" 52 ) 53 54 const ( 55 testKubeletHostname = "hostname" 56 ) 57 58 // TODO(mtaufen): below is ported from the old kubelet_node_status_test.go code, potentially add more test coverage for NodeAddress setter in future 59 func TestNodeAddress(t *testing.T) { 60 type cloudProviderType int 61 const ( 62 cloudProviderLegacy cloudProviderType = iota 63 cloudProviderExternal 64 cloudProviderNone 65 ) 66 existingNodeAddress := v1.NodeAddress{Address: "10.1.1.2"} 67 cases := []struct { 68 name string 69 hostnameOverride bool 70 nodeIP net.IP 71 secondaryNodeIP net.IP 72 cloudProviderType cloudProviderType 73 nodeAddresses []v1.NodeAddress 74 expectedAddresses []v1.NodeAddress 75 existingAnnotations map[string]string 76 expectedAnnotations map[string]string 77 shouldError bool 78 shouldSetNodeAddressBeforeTest bool 79 }{ 80 { 81 name: "A single InternalIP", 82 nodeIP: netutils.ParseIPSloppy("10.1.1.1"), 83 nodeAddresses: []v1.NodeAddress{ 84 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 85 {Type: v1.NodeHostName, Address: testKubeletHostname}, 86 }, 87 expectedAddresses: []v1.NodeAddress{ 88 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 89 {Type: v1.NodeHostName, Address: testKubeletHostname}, 90 }, 91 shouldError: false, 92 }, 93 { 94 name: "NodeIP is external", 95 nodeIP: netutils.ParseIPSloppy("55.55.55.55"), 96 nodeAddresses: []v1.NodeAddress{ 97 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 98 {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, 99 {Type: v1.NodeHostName, Address: testKubeletHostname}, 100 }, 101 expectedAddresses: []v1.NodeAddress{ 102 {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, 103 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 104 {Type: v1.NodeHostName, Address: testKubeletHostname}, 105 }, 106 shouldError: false, 107 }, 108 { 109 // Accommodating #45201 and #49202 110 name: "InternalIP and ExternalIP are the same", 111 nodeIP: netutils.ParseIPSloppy("55.55.55.55"), 112 nodeAddresses: []v1.NodeAddress{ 113 {Type: v1.NodeInternalIP, Address: "44.44.44.44"}, 114 {Type: v1.NodeExternalIP, Address: "44.44.44.44"}, 115 {Type: v1.NodeInternalIP, Address: "55.55.55.55"}, 116 {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, 117 {Type: v1.NodeHostName, Address: testKubeletHostname}, 118 }, 119 expectedAddresses: []v1.NodeAddress{ 120 {Type: v1.NodeInternalIP, Address: "55.55.55.55"}, 121 {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, 122 {Type: v1.NodeHostName, Address: testKubeletHostname}, 123 }, 124 shouldError: false, 125 }, 126 { 127 name: "An Internal/ExternalIP, an Internal/ExternalDNS", 128 nodeIP: netutils.ParseIPSloppy("10.1.1.1"), 129 nodeAddresses: []v1.NodeAddress{ 130 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 131 {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, 132 {Type: v1.NodeInternalDNS, Address: "ip-10-1-1-1.us-west-2.compute.internal"}, 133 {Type: v1.NodeExternalDNS, Address: "ec2-55-55-55-55.us-west-2.compute.amazonaws.com"}, 134 {Type: v1.NodeHostName, Address: testKubeletHostname}, 135 }, 136 expectedAddresses: []v1.NodeAddress{ 137 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 138 {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, 139 {Type: v1.NodeInternalDNS, Address: "ip-10-1-1-1.us-west-2.compute.internal"}, 140 {Type: v1.NodeExternalDNS, Address: "ec2-55-55-55-55.us-west-2.compute.amazonaws.com"}, 141 {Type: v1.NodeHostName, Address: testKubeletHostname}, 142 }, 143 shouldError: false, 144 }, 145 { 146 name: "An Internal with multiple internal IPs", 147 nodeIP: netutils.ParseIPSloppy("10.1.1.1"), 148 nodeAddresses: []v1.NodeAddress{ 149 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 150 {Type: v1.NodeInternalIP, Address: "10.2.2.2"}, 151 {Type: v1.NodeInternalIP, Address: "10.3.3.3"}, 152 {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, 153 {Type: v1.NodeHostName, Address: testKubeletHostname}, 154 }, 155 expectedAddresses: []v1.NodeAddress{ 156 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 157 {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, 158 {Type: v1.NodeHostName, Address: testKubeletHostname}, 159 }, 160 shouldError: false, 161 }, 162 { 163 name: "An InternalIP that isn't valid: should error", 164 nodeIP: netutils.ParseIPSloppy("10.2.2.2"), 165 nodeAddresses: []v1.NodeAddress{ 166 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 167 {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, 168 {Type: v1.NodeHostName, Address: testKubeletHostname}, 169 }, 170 expectedAddresses: nil, 171 shouldError: true, 172 }, 173 { 174 name: "no cloud reported hostnames", 175 nodeAddresses: []v1.NodeAddress{}, 176 expectedAddresses: []v1.NodeAddress{ 177 {Type: v1.NodeHostName, Address: testKubeletHostname}, // detected hostname is auto-added in the absence of cloud-reported hostnames 178 }, 179 shouldError: false, 180 }, 181 { 182 name: "cloud reports hostname, no override", 183 nodeAddresses: []v1.NodeAddress{ 184 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 185 {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, 186 {Type: v1.NodeHostName, Address: "cloud-host"}, 187 }, 188 expectedAddresses: []v1.NodeAddress{ 189 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 190 {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, 191 {Type: v1.NodeHostName, Address: "cloud-host"}, // cloud-reported hostname wins over detected hostname 192 }, 193 shouldError: false, 194 }, 195 { 196 name: "cloud reports hostname, nodeIP is set, no override", 197 nodeIP: netutils.ParseIPSloppy("10.1.1.1"), 198 nodeAddresses: []v1.NodeAddress{ 199 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 200 {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, 201 {Type: v1.NodeHostName, Address: "cloud-host"}, 202 }, 203 expectedAddresses: []v1.NodeAddress{ 204 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 205 {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, 206 {Type: v1.NodeHostName, Address: "cloud-host"}, // cloud-reported hostname wins over detected hostname 207 }, 208 shouldError: false, 209 }, 210 { 211 name: "cloud reports hostname, overridden", 212 nodeAddresses: []v1.NodeAddress{ 213 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 214 {Type: v1.NodeHostName, Address: "cloud-host"}, 215 {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, 216 }, 217 expectedAddresses: []v1.NodeAddress{ 218 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 219 {Type: v1.NodeHostName, Address: testKubeletHostname}, // hostname-override wins over cloud-reported hostname 220 {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, 221 }, 222 hostnameOverride: true, 223 shouldError: false, 224 }, 225 { 226 name: "cloud provider is external and nodeIP specified", 227 nodeIP: netutils.ParseIPSloppy("10.0.0.1"), 228 nodeAddresses: []v1.NodeAddress{}, 229 cloudProviderType: cloudProviderExternal, 230 expectedAddresses: []v1.NodeAddress{ 231 {Type: v1.NodeInternalIP, Address: "10.0.0.1"}, 232 {Type: v1.NodeHostName, Address: testKubeletHostname}, 233 }, 234 shouldError: false, 235 }, 236 { 237 name: "cloud provider is external and nodeIP unspecified", 238 nodeIP: netutils.ParseIPSloppy("::"), 239 nodeAddresses: []v1.NodeAddress{}, 240 cloudProviderType: cloudProviderExternal, 241 expectedAddresses: []v1.NodeAddress{ 242 {Type: v1.NodeHostName, Address: testKubeletHostname}, 243 }, 244 shouldError: false, 245 }, 246 { 247 name: "cloud provider is external and no nodeIP", 248 nodeAddresses: []v1.NodeAddress{}, 249 cloudProviderType: cloudProviderExternal, 250 expectedAddresses: []v1.NodeAddress{ 251 {Type: v1.NodeHostName, Address: testKubeletHostname}, 252 }, 253 shouldError: false, 254 }, 255 { 256 name: "cloud doesn't report hostname, no override, detected hostname mismatch", 257 nodeAddresses: []v1.NodeAddress{ 258 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 259 {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, 260 }, 261 expectedAddresses: []v1.NodeAddress{ 262 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 263 {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, 264 // detected hostname is not auto-added if it doesn't match any cloud-reported addresses 265 }, 266 shouldError: false, 267 }, 268 { 269 name: "cloud doesn't report hostname, no override, detected hostname match", 270 nodeAddresses: []v1.NodeAddress{ 271 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 272 {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, 273 {Type: v1.NodeExternalDNS, Address: testKubeletHostname}, // cloud-reported address value matches detected hostname 274 }, 275 expectedAddresses: []v1.NodeAddress{ 276 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 277 {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, 278 {Type: v1.NodeExternalDNS, Address: testKubeletHostname}, 279 {Type: v1.NodeHostName, Address: testKubeletHostname}, // detected hostname gets auto-added 280 }, 281 shouldError: false, 282 }, 283 { 284 name: "cloud doesn't report hostname, nodeIP is set, no override, detected hostname match", 285 nodeIP: netutils.ParseIPSloppy("10.1.1.1"), 286 nodeAddresses: []v1.NodeAddress{ 287 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 288 {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, 289 {Type: v1.NodeExternalDNS, Address: testKubeletHostname}, // cloud-reported address value matches detected hostname 290 }, 291 expectedAddresses: []v1.NodeAddress{ 292 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 293 {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, 294 {Type: v1.NodeExternalDNS, Address: testKubeletHostname}, 295 {Type: v1.NodeHostName, Address: testKubeletHostname}, // detected hostname gets auto-added 296 }, 297 shouldError: false, 298 }, 299 { 300 name: "cloud doesn't report hostname, nodeIP is set, no override, detected hostname match with same type as nodeIP", 301 nodeIP: netutils.ParseIPSloppy("10.1.1.1"), 302 nodeAddresses: []v1.NodeAddress{ 303 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 304 {Type: v1.NodeInternalIP, Address: testKubeletHostname}, // cloud-reported address value matches detected hostname 305 {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, 306 }, 307 expectedAddresses: []v1.NodeAddress{ 308 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 309 {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, 310 {Type: v1.NodeHostName, Address: testKubeletHostname}, // detected hostname gets auto-added 311 }, 312 shouldError: false, 313 }, 314 { 315 name: "cloud doesn't report hostname, hostname override, hostname mismatch", 316 nodeAddresses: []v1.NodeAddress{ 317 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 318 {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, 319 }, 320 expectedAddresses: []v1.NodeAddress{ 321 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 322 {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, 323 {Type: v1.NodeHostName, Address: testKubeletHostname}, // overridden hostname gets auto-added 324 }, 325 hostnameOverride: true, 326 shouldError: false, 327 }, 328 { 329 name: "Dual-stack cloud, with nodeIP, different IPv6 formats", 330 nodeIP: netutils.ParseIPSloppy("2600:1f14:1d4:d101::ba3d"), 331 nodeAddresses: []v1.NodeAddress{ 332 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 333 {Type: v1.NodeInternalIP, Address: "2600:1f14:1d4:d101:0:0:0:ba3d"}, 334 {Type: v1.NodeHostName, Address: testKubeletHostname}, 335 }, 336 expectedAddresses: []v1.NodeAddress{ 337 {Type: v1.NodeInternalIP, Address: "2600:1f14:1d4:d101:0:0:0:ba3d"}, 338 {Type: v1.NodeHostName, Address: testKubeletHostname}, 339 }, 340 shouldError: false, 341 }, 342 { 343 name: "Dual-stack cloud, IPv4 first, no nodeIP", 344 nodeAddresses: []v1.NodeAddress{ 345 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 346 {Type: v1.NodeInternalIP, Address: "fc01:1234::5678"}, 347 {Type: v1.NodeHostName, Address: testKubeletHostname}, 348 }, 349 expectedAddresses: []v1.NodeAddress{ 350 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 351 {Type: v1.NodeInternalIP, Address: "fc01:1234::5678"}, 352 {Type: v1.NodeHostName, Address: testKubeletHostname}, 353 }, 354 shouldError: false, 355 }, 356 { 357 name: "Dual-stack cloud, IPv6 first, no nodeIP", 358 nodeAddresses: []v1.NodeAddress{ 359 {Type: v1.NodeInternalIP, Address: "fc01:1234::5678"}, 360 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 361 {Type: v1.NodeHostName, Address: testKubeletHostname}, 362 }, 363 expectedAddresses: []v1.NodeAddress{ 364 {Type: v1.NodeInternalIP, Address: "fc01:1234::5678"}, 365 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 366 {Type: v1.NodeHostName, Address: testKubeletHostname}, 367 }, 368 shouldError: false, 369 }, 370 { 371 name: "Dual-stack cloud, IPv4 first, request IPv4", 372 nodeIP: netutils.ParseIPSloppy("0.0.0.0"), 373 nodeAddresses: []v1.NodeAddress{ 374 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 375 {Type: v1.NodeInternalIP, Address: "fc01:1234::5678"}, 376 {Type: v1.NodeHostName, Address: testKubeletHostname}, 377 }, 378 expectedAddresses: []v1.NodeAddress{ 379 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 380 {Type: v1.NodeHostName, Address: testKubeletHostname}, 381 {Type: v1.NodeInternalIP, Address: "fc01:1234::5678"}, 382 }, 383 shouldError: false, 384 }, 385 { 386 name: "Dual-stack cloud, IPv6 first, request IPv4", 387 nodeIP: netutils.ParseIPSloppy("0.0.0.0"), 388 nodeAddresses: []v1.NodeAddress{ 389 {Type: v1.NodeInternalIP, Address: "fc01:1234::5678"}, 390 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 391 {Type: v1.NodeHostName, Address: testKubeletHostname}, 392 }, 393 expectedAddresses: []v1.NodeAddress{ 394 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 395 {Type: v1.NodeHostName, Address: testKubeletHostname}, 396 {Type: v1.NodeInternalIP, Address: "fc01:1234::5678"}, 397 }, 398 shouldError: false, 399 }, 400 { 401 name: "Dual-stack cloud, IPv4 first, request IPv6", 402 nodeIP: netutils.ParseIPSloppy("::"), 403 nodeAddresses: []v1.NodeAddress{ 404 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 405 {Type: v1.NodeInternalIP, Address: "fc01:1234::5678"}, 406 {Type: v1.NodeHostName, Address: testKubeletHostname}, 407 }, 408 expectedAddresses: []v1.NodeAddress{ 409 {Type: v1.NodeInternalIP, Address: "fc01:1234::5678"}, 410 {Type: v1.NodeHostName, Address: testKubeletHostname}, 411 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 412 }, 413 shouldError: false, 414 }, 415 { 416 name: "Dual-stack cloud, IPv6 first, request IPv6", 417 nodeIP: netutils.ParseIPSloppy("::"), 418 nodeAddresses: []v1.NodeAddress{ 419 {Type: v1.NodeInternalIP, Address: "fc01:1234::5678"}, 420 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 421 {Type: v1.NodeHostName, Address: testKubeletHostname}, 422 }, 423 expectedAddresses: []v1.NodeAddress{ 424 {Type: v1.NodeInternalIP, Address: "fc01:1234::5678"}, 425 {Type: v1.NodeHostName, Address: testKubeletHostname}, 426 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 427 }, 428 shouldError: false, 429 }, 430 { 431 name: "Legacy cloud provider gets nodeIP annotation", 432 nodeIP: netutils.ParseIPSloppy("10.1.1.1"), 433 cloudProviderType: cloudProviderLegacy, 434 nodeAddresses: []v1.NodeAddress{ 435 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 436 {Type: v1.NodeHostName, Address: testKubeletHostname}, 437 }, 438 expectedAddresses: []v1.NodeAddress{ 439 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 440 {Type: v1.NodeHostName, Address: testKubeletHostname}, 441 }, 442 expectedAnnotations: map[string]string{ 443 "alpha.kubernetes.io/provided-node-ip": "10.1.1.1", 444 }, 445 shouldError: false, 446 }, 447 { 448 name: "External cloud provider gets nodeIP annotation", 449 nodeIP: netutils.ParseIPSloppy("10.1.1.1"), 450 cloudProviderType: cloudProviderExternal, 451 nodeAddresses: []v1.NodeAddress{ 452 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 453 {Type: v1.NodeHostName, Address: testKubeletHostname}, 454 }, 455 expectedAddresses: []v1.NodeAddress{ 456 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 457 {Type: v1.NodeHostName, Address: testKubeletHostname}, 458 }, 459 expectedAnnotations: map[string]string{ 460 "alpha.kubernetes.io/provided-node-ip": "10.1.1.1", 461 }, 462 shouldError: false, 463 }, 464 { 465 name: "External cloud provider, node address is already set", 466 nodeIP: netutils.ParseIPSloppy("10.1.1.1"), 467 cloudProviderType: cloudProviderExternal, 468 nodeAddresses: []v1.NodeAddress{existingNodeAddress}, 469 expectedAddresses: []v1.NodeAddress{existingNodeAddress}, 470 shouldError: true, 471 shouldSetNodeAddressBeforeTest: true, 472 }, 473 { 474 name: "No cloud provider does not get nodeIP annotation", 475 nodeIP: netutils.ParseIPSloppy("10.1.1.1"), 476 cloudProviderType: cloudProviderNone, 477 nodeAddresses: []v1.NodeAddress{ 478 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 479 {Type: v1.NodeHostName, Address: testKubeletHostname}, 480 }, 481 expectedAddresses: []v1.NodeAddress{ 482 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 483 {Type: v1.NodeHostName, Address: testKubeletHostname}, 484 }, 485 expectedAnnotations: map[string]string{}, 486 shouldError: false, 487 }, 488 { 489 name: "Stale nodeIP annotation is removed when not using cloud provider", 490 nodeIP: netutils.ParseIPSloppy("10.1.1.1"), 491 cloudProviderType: cloudProviderNone, 492 nodeAddresses: []v1.NodeAddress{ 493 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 494 {Type: v1.NodeHostName, Address: testKubeletHostname}, 495 }, 496 expectedAddresses: []v1.NodeAddress{ 497 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 498 {Type: v1.NodeHostName, Address: testKubeletHostname}, 499 }, 500 existingAnnotations: map[string]string{ 501 "alpha.kubernetes.io/provided-node-ip": "10.1.1.3", 502 }, 503 expectedAnnotations: map[string]string{}, 504 shouldError: false, 505 }, 506 { 507 name: "Stale nodeIP annotation is removed when using cloud provider but no --node-ip", 508 nodeIP: nil, 509 cloudProviderType: cloudProviderLegacy, 510 nodeAddresses: []v1.NodeAddress{ 511 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 512 {Type: v1.NodeHostName, Address: testKubeletHostname}, 513 }, 514 expectedAddresses: []v1.NodeAddress{ 515 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 516 {Type: v1.NodeHostName, Address: testKubeletHostname}, 517 }, 518 existingAnnotations: map[string]string{ 519 "alpha.kubernetes.io/provided-node-ip": "10.1.1.1", 520 }, 521 expectedAnnotations: map[string]string{}, 522 shouldError: false, 523 }, 524 { 525 name: "Incorrect nodeIP annotation is fixed", 526 nodeIP: netutils.ParseIPSloppy("10.1.1.1"), 527 cloudProviderType: cloudProviderExternal, 528 nodeAddresses: []v1.NodeAddress{ 529 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 530 {Type: v1.NodeHostName, Address: testKubeletHostname}, 531 }, 532 expectedAddresses: []v1.NodeAddress{ 533 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 534 {Type: v1.NodeHostName, Address: testKubeletHostname}, 535 }, 536 existingAnnotations: map[string]string{ 537 "alpha.kubernetes.io/provided-node-ip": "10.1.1.3", 538 }, 539 expectedAnnotations: map[string]string{ 540 "alpha.kubernetes.io/provided-node-ip": "10.1.1.1", 541 }, 542 shouldError: false, 543 }, 544 { 545 // We don't have to test "legacy cloud provider with dual-stack 546 // IPs" etc because we won't have gotten this far with an invalid 547 // config like that. 548 name: "Dual-stack cloud, with dual-stack nodeIPs", 549 nodeIP: netutils.ParseIPSloppy("2600:1f14:1d4:d101::ba3d"), 550 secondaryNodeIP: netutils.ParseIPSloppy("10.1.1.2"), 551 cloudProviderType: cloudProviderExternal, 552 nodeAddresses: []v1.NodeAddress{ 553 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 554 {Type: v1.NodeInternalIP, Address: "10.1.1.2"}, 555 {Type: v1.NodeInternalIP, Address: "2600:1f14:1d4:d101::ba3d"}, 556 {Type: v1.NodeHostName, Address: testKubeletHostname}, 557 }, 558 expectedAddresses: []v1.NodeAddress{ 559 {Type: v1.NodeInternalIP, Address: "2600:1f14:1d4:d101::ba3d"}, 560 {Type: v1.NodeInternalIP, Address: "10.1.1.2"}, 561 {Type: v1.NodeHostName, Address: testKubeletHostname}, 562 }, 563 expectedAnnotations: map[string]string{ 564 "alpha.kubernetes.io/provided-node-ip": "2600:1f14:1d4:d101::ba3d,10.1.1.2", 565 }, 566 shouldError: false, 567 }, 568 { 569 name: "Upgrade to cloud dual-stack nodeIPs", 570 nodeIP: netutils.ParseIPSloppy("10.1.1.1"), 571 secondaryNodeIP: netutils.ParseIPSloppy("2600:1f14:1d4:d101::ba3d"), 572 cloudProviderType: cloudProviderExternal, 573 nodeAddresses: []v1.NodeAddress{ 574 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 575 {Type: v1.NodeInternalIP, Address: "2600:1f14:1d4:d101::ba3d"}, 576 {Type: v1.NodeHostName, Address: testKubeletHostname}, 577 }, 578 expectedAddresses: []v1.NodeAddress{ 579 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 580 {Type: v1.NodeInternalIP, Address: "2600:1f14:1d4:d101::ba3d"}, 581 {Type: v1.NodeHostName, Address: testKubeletHostname}, 582 }, 583 existingAnnotations: map[string]string{ 584 "alpha.kubernetes.io/provided-node-ip": "10.1.1.1", 585 }, 586 expectedAnnotations: map[string]string{ 587 "alpha.kubernetes.io/provided-node-ip": "10.1.1.1,2600:1f14:1d4:d101::ba3d", 588 }, 589 shouldError: false, 590 }, 591 { 592 name: "Downgrade from cloud dual-stack nodeIPs", 593 nodeIP: netutils.ParseIPSloppy("10.1.1.1"), 594 cloudProviderType: cloudProviderExternal, 595 nodeAddresses: []v1.NodeAddress{ 596 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 597 {Type: v1.NodeInternalIP, Address: "2600:1f14:1d4:d101::ba3d"}, 598 {Type: v1.NodeHostName, Address: testKubeletHostname}, 599 }, 600 expectedAddresses: []v1.NodeAddress{ 601 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 602 {Type: v1.NodeHostName, Address: testKubeletHostname}, 603 }, 604 existingAnnotations: map[string]string{ 605 "alpha.kubernetes.io/provided-node-ip": "10.1.1.1,2600:1f14:1d4:d101::ba3d", 606 }, 607 expectedAnnotations: map[string]string{ 608 "alpha.kubernetes.io/provided-node-ip": "10.1.1.1", 609 }, 610 shouldError: false, 611 }, 612 } 613 for _, testCase := range cases { 614 t.Run(testCase.name, func(t *testing.T) { 615 ctx := context.Background() 616 // testCase setup 617 existingNode := &v1.Node{ 618 ObjectMeta: metav1.ObjectMeta{ 619 Name: testKubeletHostname, 620 Annotations: testCase.existingAnnotations, 621 }, 622 Spec: v1.NodeSpec{}, 623 Status: v1.NodeStatus{ 624 Addresses: []v1.NodeAddress{}, 625 }, 626 } 627 628 if testCase.shouldSetNodeAddressBeforeTest { 629 existingNode.Status.Addresses = append(existingNode.Status.Addresses, existingNodeAddress) 630 } 631 632 nodeIPValidator := func(nodeIP net.IP) error { 633 return nil 634 } 635 hostname := testKubeletHostname 636 637 nodeAddressesFunc := func() ([]v1.NodeAddress, error) { 638 return testCase.nodeAddresses, nil 639 } 640 641 // cloud provider is expected to be nil if external provider is set or there is no cloud provider 642 var cloud cloudprovider.Interface 643 if testCase.cloudProviderType == cloudProviderLegacy { 644 cloud = &fakecloud.Cloud{ 645 Addresses: testCase.nodeAddresses, 646 Err: nil, 647 } 648 } 649 650 nodeIPs := []net.IP{testCase.nodeIP} 651 if testCase.secondaryNodeIP != nil { 652 nodeIPs = append(nodeIPs, testCase.secondaryNodeIP) 653 } 654 655 // construct setter 656 setter := NodeAddress(nodeIPs, 657 nodeIPValidator, 658 hostname, 659 testCase.hostnameOverride, 660 testCase.cloudProviderType == cloudProviderExternal, 661 cloud, 662 nodeAddressesFunc) 663 664 // call setter on existing node 665 err := setter(ctx, existingNode) 666 if err != nil && !testCase.shouldError { 667 t.Fatalf("unexpected error: %v", err) 668 } else if err != nil && testCase.shouldError { 669 // expected an error, and got one, so just return early here 670 return 671 } 672 673 assert.True(t, apiequality.Semantic.DeepEqual(testCase.expectedAddresses, existingNode.Status.Addresses), 674 "Diff: %s", cmp.Diff(testCase.expectedAddresses, existingNode.Status.Addresses)) 675 if testCase.expectedAnnotations != nil { 676 assert.True(t, apiequality.Semantic.DeepEqual(testCase.expectedAnnotations, existingNode.Annotations), 677 "Diff: %s", cmp.Diff(testCase.expectedAnnotations, existingNode.Annotations)) 678 } 679 }) 680 } 681 } 682 683 // We can't test failure or autodetection cases here because the relevant code isn't mockable 684 func TestNodeAddress_NoCloudProvider(t *testing.T) { 685 cases := []struct { 686 name string 687 nodeIPs []net.IP 688 expectedAddresses []v1.NodeAddress 689 shouldError bool 690 }{ 691 { 692 name: "Single --node-ip", 693 nodeIPs: []net.IP{netutils.ParseIPSloppy("10.1.1.1")}, 694 expectedAddresses: []v1.NodeAddress{ 695 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 696 {Type: v1.NodeHostName, Address: testKubeletHostname}, 697 }, 698 }, 699 { 700 name: "Invalid single --node-ip (using loopback)", 701 nodeIPs: []net.IP{netutils.ParseIPSloppy("127.0.0.1")}, 702 shouldError: true, 703 }, 704 { 705 name: "Dual --node-ips", 706 nodeIPs: []net.IP{netutils.ParseIPSloppy("10.1.1.1"), netutils.ParseIPSloppy("fd01::1234")}, 707 expectedAddresses: []v1.NodeAddress{ 708 {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, 709 {Type: v1.NodeInternalIP, Address: "fd01::1234"}, 710 {Type: v1.NodeHostName, Address: testKubeletHostname}, 711 }, 712 }, 713 { 714 name: "Dual --node-ips but with invalid secondary IP (using multicast IP)", 715 nodeIPs: []net.IP{netutils.ParseIPSloppy("10.1.1.1"), netutils.ParseIPSloppy("224.0.0.0")}, 716 shouldError: true, 717 }, 718 } 719 for _, testCase := range cases { 720 t.Run(testCase.name, func(t *testing.T) { 721 ctx := context.Background() 722 // testCase setup 723 existingNode := &v1.Node{ 724 ObjectMeta: metav1.ObjectMeta{Name: testKubeletHostname, Annotations: make(map[string]string)}, 725 Spec: v1.NodeSpec{}, 726 Status: v1.NodeStatus{ 727 Addresses: []v1.NodeAddress{}, 728 }, 729 } 730 731 nodeIPValidator := func(nodeIP net.IP) error { 732 if nodeIP.IsLoopback() { 733 return fmt.Errorf("nodeIP can't be loopback address") 734 } else if nodeIP.IsMulticast() { 735 return fmt.Errorf("nodeIP can't be a multicast address") 736 } 737 return nil 738 } 739 nodeAddressesFunc := func() ([]v1.NodeAddress, error) { 740 return nil, fmt.Errorf("not reached") 741 } 742 743 // construct setter 744 setter := NodeAddress(testCase.nodeIPs, 745 nodeIPValidator, 746 testKubeletHostname, 747 false, // hostnameOverridden 748 false, // externalCloudProvider 749 nil, // cloud 750 nodeAddressesFunc) 751 752 // call setter on existing node 753 err := setter(ctx, existingNode) 754 if testCase.shouldError && err == nil { 755 t.Fatal("expected error but no error returned") 756 } 757 if err != nil && !testCase.shouldError { 758 t.Fatalf("unexpected error: %v", err) 759 } 760 761 assert.True(t, apiequality.Semantic.DeepEqual(testCase.expectedAddresses, existingNode.Status.Addresses), 762 "Diff: %s", cmp.Diff(testCase.expectedAddresses, existingNode.Status.Addresses)) 763 }) 764 } 765 } 766 767 func TestMachineInfo(t *testing.T) { 768 const nodeName = "test-node" 769 770 type dprc struct { 771 capacity v1.ResourceList 772 allocatable v1.ResourceList 773 inactive []string 774 } 775 776 cases := []struct { 777 desc string 778 node *v1.Node 779 maxPods int 780 podsPerCore int 781 machineInfo *cadvisorapiv1.MachineInfo 782 machineInfoError error 783 capacity v1.ResourceList 784 devicePluginResourceCapacity dprc 785 nodeAllocatableReservation v1.ResourceList 786 expectNode *v1.Node 787 expectEvents []testEvent 788 disableLocalStorageCapacityIsolation bool 789 }{ 790 { 791 desc: "machine identifiers, basic capacity and allocatable", 792 node: &v1.Node{}, 793 maxPods: 110, 794 machineInfo: &cadvisorapiv1.MachineInfo{ 795 MachineID: "MachineID", 796 SystemUUID: "SystemUUID", 797 NumCores: 2, 798 MemoryCapacity: 1024, 799 }, 800 expectNode: &v1.Node{ 801 Status: v1.NodeStatus{ 802 NodeInfo: v1.NodeSystemInfo{ 803 MachineID: "MachineID", 804 SystemUUID: "SystemUUID", 805 }, 806 Capacity: v1.ResourceList{ 807 v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 808 v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), 809 v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), 810 }, 811 Allocatable: v1.ResourceList{ 812 v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 813 v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), 814 v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), 815 }, 816 }, 817 }, 818 }, 819 { 820 desc: "podsPerCore greater than zero, but less than maxPods/cores", 821 node: &v1.Node{}, 822 maxPods: 10, 823 podsPerCore: 4, 824 machineInfo: &cadvisorapiv1.MachineInfo{ 825 NumCores: 2, 826 MemoryCapacity: 1024, 827 }, 828 expectNode: &v1.Node{ 829 Status: v1.NodeStatus{ 830 Capacity: v1.ResourceList{ 831 v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 832 v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), 833 v1.ResourcePods: *resource.NewQuantity(8, resource.DecimalSI), 834 }, 835 Allocatable: v1.ResourceList{ 836 v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 837 v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), 838 v1.ResourcePods: *resource.NewQuantity(8, resource.DecimalSI), 839 }, 840 }, 841 }, 842 }, 843 { 844 desc: "podsPerCore greater than maxPods/cores", 845 node: &v1.Node{}, 846 maxPods: 10, 847 podsPerCore: 6, 848 machineInfo: &cadvisorapiv1.MachineInfo{ 849 NumCores: 2, 850 MemoryCapacity: 1024, 851 }, 852 expectNode: &v1.Node{ 853 Status: v1.NodeStatus{ 854 Capacity: v1.ResourceList{ 855 v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 856 v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), 857 v1.ResourcePods: *resource.NewQuantity(10, resource.DecimalSI), 858 }, 859 Allocatable: v1.ResourceList{ 860 v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 861 v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), 862 v1.ResourcePods: *resource.NewQuantity(10, resource.DecimalSI), 863 }, 864 }, 865 }, 866 }, 867 { 868 desc: "allocatable should equal capacity minus reservations", 869 node: &v1.Node{}, 870 maxPods: 110, 871 machineInfo: &cadvisorapiv1.MachineInfo{ 872 NumCores: 2, 873 MemoryCapacity: 1024, 874 }, 875 nodeAllocatableReservation: v1.ResourceList{ 876 // reserve 1 unit for each resource 877 v1.ResourceCPU: *resource.NewMilliQuantity(1, resource.DecimalSI), 878 v1.ResourceMemory: *resource.NewQuantity(1, resource.BinarySI), 879 v1.ResourcePods: *resource.NewQuantity(1, resource.DecimalSI), 880 v1.ResourceEphemeralStorage: *resource.NewQuantity(1, resource.BinarySI), 881 }, 882 expectNode: &v1.Node{ 883 Status: v1.NodeStatus{ 884 Capacity: v1.ResourceList{ 885 v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 886 v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), 887 v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), 888 }, 889 Allocatable: v1.ResourceList{ 890 v1.ResourceCPU: *resource.NewMilliQuantity(1999, resource.DecimalSI), 891 v1.ResourceMemory: *resource.NewQuantity(1023, resource.BinarySI), 892 v1.ResourcePods: *resource.NewQuantity(109, resource.DecimalSI), 893 }, 894 }, 895 }, 896 }, 897 { 898 desc: "allocatable memory does not double-count hugepages reservations", 899 node: &v1.Node{ 900 Status: v1.NodeStatus{ 901 Capacity: v1.ResourceList{ 902 // it's impossible on any real system to reserve 1 byte, 903 // but we just need to test that the setter does the math 904 v1.ResourceHugePagesPrefix + "test": *resource.NewQuantity(1, resource.BinarySI), 905 }, 906 }, 907 }, 908 maxPods: 110, 909 machineInfo: &cadvisorapiv1.MachineInfo{ 910 NumCores: 2, 911 MemoryCapacity: 1024, 912 }, 913 expectNode: &v1.Node{ 914 Status: v1.NodeStatus{ 915 Capacity: v1.ResourceList{ 916 v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 917 v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), 918 v1.ResourceHugePagesPrefix + "test": *resource.NewQuantity(1, resource.BinarySI), 919 v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), 920 }, 921 Allocatable: v1.ResourceList{ 922 v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 923 // memory has 1-unit difference for hugepages reservation 924 v1.ResourceMemory: *resource.NewQuantity(1023, resource.BinarySI), 925 v1.ResourceHugePagesPrefix + "test": *resource.NewQuantity(1, resource.BinarySI), 926 v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), 927 }, 928 }, 929 }, 930 }, 931 { 932 desc: "negative capacity resources should be set to 0 in allocatable", 933 node: &v1.Node{ 934 Status: v1.NodeStatus{ 935 Capacity: v1.ResourceList{ 936 "negative-resource": *resource.NewQuantity(-1, resource.BinarySI), 937 }, 938 }, 939 }, 940 maxPods: 110, 941 machineInfo: &cadvisorapiv1.MachineInfo{ 942 NumCores: 2, 943 MemoryCapacity: 1024, 944 }, 945 expectNode: &v1.Node{ 946 Status: v1.NodeStatus{ 947 Capacity: v1.ResourceList{ 948 v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 949 v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), 950 v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), 951 "negative-resource": *resource.NewQuantity(-1, resource.BinarySI), 952 }, 953 Allocatable: v1.ResourceList{ 954 v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 955 v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), 956 v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), 957 "negative-resource": *resource.NewQuantity(0, resource.BinarySI), 958 }, 959 }, 960 }, 961 }, 962 { 963 desc: "hugepages reservation greater than node memory capacity should result in memory capacity set to 0", 964 node: &v1.Node{ 965 Status: v1.NodeStatus{ 966 Capacity: v1.ResourceList{ 967 v1.ResourceHugePagesPrefix + "test": *resource.NewQuantity(1025, resource.BinarySI), 968 }, 969 }, 970 }, 971 maxPods: 110, 972 machineInfo: &cadvisorapiv1.MachineInfo{ 973 NumCores: 2, 974 MemoryCapacity: 1024, 975 }, 976 expectNode: &v1.Node{ 977 Status: v1.NodeStatus{ 978 Capacity: v1.ResourceList{ 979 v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 980 v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), 981 v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), 982 v1.ResourceHugePagesPrefix + "test": *resource.NewQuantity(1025, resource.BinarySI), 983 }, 984 Allocatable: v1.ResourceList{ 985 v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 986 v1.ResourceMemory: *resource.NewQuantity(0, resource.BinarySI), 987 v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), 988 v1.ResourceHugePagesPrefix + "test": *resource.NewQuantity(1025, resource.BinarySI), 989 }, 990 }, 991 }, 992 }, 993 { 994 desc: "ephemeral storage is reflected in capacity and allocatable", 995 node: &v1.Node{}, 996 maxPods: 110, 997 machineInfo: &cadvisorapiv1.MachineInfo{ 998 NumCores: 2, 999 MemoryCapacity: 1024, 1000 }, 1001 capacity: v1.ResourceList{ 1002 v1.ResourceEphemeralStorage: *resource.NewQuantity(5000, resource.BinarySI), 1003 }, 1004 expectNode: &v1.Node{ 1005 Status: v1.NodeStatus{ 1006 Capacity: v1.ResourceList{ 1007 v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 1008 v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), 1009 v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), 1010 v1.ResourceEphemeralStorage: *resource.NewQuantity(5000, resource.BinarySI), 1011 }, 1012 Allocatable: v1.ResourceList{ 1013 v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 1014 v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), 1015 v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), 1016 v1.ResourceEphemeralStorage: *resource.NewQuantity(5000, resource.BinarySI), 1017 }, 1018 }, 1019 }, 1020 }, 1021 { 1022 desc: "ephemeral storage is not reflected in capacity and allocatable because localStorageCapacityIsolation is disabled", 1023 node: &v1.Node{}, 1024 maxPods: 110, 1025 machineInfo: &cadvisorapiv1.MachineInfo{ 1026 NumCores: 2, 1027 MemoryCapacity: 1024, 1028 }, 1029 capacity: v1.ResourceList{ 1030 v1.ResourceEphemeralStorage: *resource.NewQuantity(5000, resource.BinarySI), 1031 }, 1032 expectNode: &v1.Node{ 1033 Status: v1.NodeStatus{ 1034 Capacity: v1.ResourceList{ 1035 v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 1036 v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), 1037 v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), 1038 v1.ResourceEphemeralStorage: *resource.NewQuantity(5000, resource.BinarySI), 1039 }, 1040 Allocatable: v1.ResourceList{ 1041 v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 1042 v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), 1043 v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), 1044 v1.ResourceEphemeralStorage: *resource.NewQuantity(5000, resource.BinarySI), 1045 }, 1046 }, 1047 }, 1048 disableLocalStorageCapacityIsolation: true, 1049 }, 1050 { 1051 desc: "device plugin resources are reflected in capacity and allocatable", 1052 node: &v1.Node{}, 1053 maxPods: 110, 1054 machineInfo: &cadvisorapiv1.MachineInfo{ 1055 NumCores: 2, 1056 MemoryCapacity: 1024, 1057 }, 1058 devicePluginResourceCapacity: dprc{ 1059 capacity: v1.ResourceList{ 1060 "device-plugin": *resource.NewQuantity(1, resource.BinarySI), 1061 }, 1062 allocatable: v1.ResourceList{ 1063 "device-plugin": *resource.NewQuantity(1, resource.BinarySI), 1064 }, 1065 }, 1066 expectNode: &v1.Node{ 1067 Status: v1.NodeStatus{ 1068 Capacity: v1.ResourceList{ 1069 v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 1070 v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), 1071 v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), 1072 "device-plugin": *resource.NewQuantity(1, resource.BinarySI), 1073 }, 1074 Allocatable: v1.ResourceList{ 1075 v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 1076 v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), 1077 v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), 1078 "device-plugin": *resource.NewQuantity(1, resource.BinarySI), 1079 }, 1080 }, 1081 }, 1082 }, 1083 { 1084 desc: "inactive device plugin resources should have their capacity set to 0", 1085 node: &v1.Node{ 1086 Status: v1.NodeStatus{ 1087 Capacity: v1.ResourceList{ 1088 "inactive": *resource.NewQuantity(1, resource.BinarySI), 1089 }, 1090 }, 1091 }, 1092 maxPods: 110, 1093 machineInfo: &cadvisorapiv1.MachineInfo{ 1094 NumCores: 2, 1095 MemoryCapacity: 1024, 1096 }, 1097 devicePluginResourceCapacity: dprc{ 1098 inactive: []string{"inactive"}, 1099 }, 1100 expectNode: &v1.Node{ 1101 Status: v1.NodeStatus{ 1102 Capacity: v1.ResourceList{ 1103 v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 1104 v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), 1105 v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), 1106 "inactive": *resource.NewQuantity(0, resource.BinarySI), 1107 }, 1108 Allocatable: v1.ResourceList{ 1109 v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 1110 v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), 1111 v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), 1112 "inactive": *resource.NewQuantity(0, resource.BinarySI), 1113 }, 1114 }, 1115 }, 1116 }, 1117 { 1118 desc: "extended resources not present in capacity are removed from allocatable", 1119 node: &v1.Node{ 1120 Status: v1.NodeStatus{ 1121 Allocatable: v1.ResourceList{ 1122 "example.com/extended": *resource.NewQuantity(1, resource.BinarySI), 1123 }, 1124 }, 1125 }, 1126 maxPods: 110, 1127 machineInfo: &cadvisorapiv1.MachineInfo{ 1128 NumCores: 2, 1129 MemoryCapacity: 1024, 1130 }, 1131 expectNode: &v1.Node{ 1132 Status: v1.NodeStatus{ 1133 Capacity: v1.ResourceList{ 1134 v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 1135 v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), 1136 v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), 1137 }, 1138 Allocatable: v1.ResourceList{ 1139 v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 1140 v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), 1141 v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), 1142 }, 1143 }, 1144 }, 1145 }, 1146 { 1147 desc: "on failure to get machine info, allocatable and capacity for memory and cpu are set to 0, pods to maxPods", 1148 node: &v1.Node{}, 1149 maxPods: 110, 1150 // podsPerCore is not accounted for when getting machine info fails 1151 podsPerCore: 1, 1152 machineInfoError: fmt.Errorf("foo"), 1153 expectNode: &v1.Node{ 1154 Status: v1.NodeStatus{ 1155 Capacity: v1.ResourceList{ 1156 v1.ResourceCPU: *resource.NewMilliQuantity(0, resource.DecimalSI), 1157 v1.ResourceMemory: resource.MustParse("0Gi"), 1158 v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), 1159 }, 1160 Allocatable: v1.ResourceList{ 1161 v1.ResourceCPU: *resource.NewMilliQuantity(0, resource.DecimalSI), 1162 v1.ResourceMemory: resource.MustParse("0Gi"), 1163 v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), 1164 }, 1165 }, 1166 }, 1167 }, 1168 { 1169 desc: "node reboot event is recorded", 1170 node: &v1.Node{ 1171 Status: v1.NodeStatus{ 1172 NodeInfo: v1.NodeSystemInfo{ 1173 BootID: "foo", 1174 }, 1175 }, 1176 }, 1177 maxPods: 110, 1178 machineInfo: &cadvisorapiv1.MachineInfo{ 1179 BootID: "bar", 1180 NumCores: 2, 1181 MemoryCapacity: 1024, 1182 }, 1183 expectNode: &v1.Node{ 1184 Status: v1.NodeStatus{ 1185 NodeInfo: v1.NodeSystemInfo{ 1186 BootID: "bar", 1187 }, 1188 Capacity: v1.ResourceList{ 1189 v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 1190 v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), 1191 v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), 1192 }, 1193 Allocatable: v1.ResourceList{ 1194 v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 1195 v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), 1196 v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), 1197 }, 1198 }, 1199 }, 1200 expectEvents: []testEvent{ 1201 { 1202 eventType: v1.EventTypeWarning, 1203 event: events.NodeRebooted, 1204 message: fmt.Sprintf("Node %s has been rebooted, boot id: %s", nodeName, "bar"), 1205 }, 1206 }, 1207 }, 1208 } 1209 1210 for _, tc := range cases { 1211 t.Run(tc.desc, func(t *testing.T) { 1212 ctx := context.Background() 1213 machineInfoFunc := func() (*cadvisorapiv1.MachineInfo, error) { 1214 return tc.machineInfo, tc.machineInfoError 1215 } 1216 capacityFunc := func(localStorageCapacityIsolation bool) v1.ResourceList { 1217 return tc.capacity 1218 } 1219 devicePluginResourceCapacityFunc := func() (v1.ResourceList, v1.ResourceList, []string) { 1220 c := tc.devicePluginResourceCapacity 1221 return c.capacity, c.allocatable, c.inactive 1222 } 1223 nodeAllocatableReservationFunc := func() v1.ResourceList { 1224 return tc.nodeAllocatableReservation 1225 } 1226 1227 events := []testEvent{} 1228 recordEventFunc := func(eventType, event, message string) { 1229 events = append(events, testEvent{ 1230 eventType: eventType, 1231 event: event, 1232 message: message, 1233 }) 1234 } 1235 // construct setter 1236 setter := MachineInfo(nodeName, tc.maxPods, tc.podsPerCore, machineInfoFunc, capacityFunc, 1237 devicePluginResourceCapacityFunc, nodeAllocatableReservationFunc, recordEventFunc, tc.disableLocalStorageCapacityIsolation) 1238 // call setter on node 1239 if err := setter(ctx, tc.node); err != nil { 1240 t.Fatalf("unexpected error: %v", err) 1241 } 1242 // check expected node 1243 assert.True(t, apiequality.Semantic.DeepEqual(tc.expectNode, tc.node), 1244 "Diff: %s", cmp.Diff(tc.expectNode, tc.node)) 1245 // check expected events 1246 require.Equal(t, len(tc.expectEvents), len(events)) 1247 for i := range tc.expectEvents { 1248 assert.Equal(t, tc.expectEvents[i], events[i]) 1249 } 1250 }) 1251 } 1252 1253 } 1254 1255 func TestVersionInfo(t *testing.T) { 1256 cases := []struct { 1257 desc string 1258 node *v1.Node 1259 versionInfo *cadvisorapiv1.VersionInfo 1260 versionInfoError error 1261 runtimeType string 1262 runtimeVersion kubecontainer.Version 1263 runtimeVersionError error 1264 expectNode *v1.Node 1265 expectError error 1266 kubeProxyVersion bool 1267 }{ 1268 { 1269 desc: "versions set in node info", 1270 node: &v1.Node{}, 1271 versionInfo: &cadvisorapiv1.VersionInfo{ 1272 KernelVersion: "KernelVersion", 1273 ContainerOsVersion: "ContainerOSVersion", 1274 }, 1275 runtimeType: "RuntimeType", 1276 runtimeVersion: &kubecontainertest.FakeVersion{ 1277 Version: "RuntimeVersion", 1278 }, 1279 expectNode: &v1.Node{ 1280 Status: v1.NodeStatus{ 1281 NodeInfo: v1.NodeSystemInfo{ 1282 KernelVersion: "KernelVersion", 1283 OSImage: "ContainerOSVersion", 1284 ContainerRuntimeVersion: "RuntimeType://RuntimeVersion", 1285 KubeletVersion: version.Get().String(), 1286 KubeProxyVersion: version.Get().String(), 1287 }, 1288 }, 1289 }, 1290 kubeProxyVersion: true, 1291 }, 1292 { 1293 desc: "error getting version info", 1294 node: &v1.Node{}, 1295 versionInfoError: fmt.Errorf("foo"), 1296 expectNode: &v1.Node{}, 1297 expectError: fmt.Errorf("error getting version info: foo"), 1298 kubeProxyVersion: true, 1299 }, 1300 { 1301 desc: "error getting runtime version results in Unknown runtime", 1302 node: &v1.Node{}, 1303 versionInfo: &cadvisorapiv1.VersionInfo{}, 1304 runtimeType: "RuntimeType", 1305 runtimeVersionError: fmt.Errorf("foo"), 1306 expectNode: &v1.Node{ 1307 Status: v1.NodeStatus{ 1308 NodeInfo: v1.NodeSystemInfo{ 1309 ContainerRuntimeVersion: "RuntimeType://Unknown", 1310 KubeletVersion: version.Get().String(), 1311 KubeProxyVersion: version.Get().String(), 1312 }, 1313 }, 1314 }, 1315 kubeProxyVersion: true, 1316 }, 1317 { 1318 desc: "DisableNodeKubeProxyVersion FeatureGate enable, versions set in node info", 1319 node: &v1.Node{}, 1320 versionInfo: &cadvisorapiv1.VersionInfo{ 1321 KernelVersion: "KernelVersion", 1322 ContainerOsVersion: "ContainerOSVersion", 1323 }, 1324 runtimeType: "RuntimeType", 1325 runtimeVersion: &kubecontainertest.FakeVersion{ 1326 Version: "RuntimeVersion", 1327 }, 1328 expectNode: &v1.Node{ 1329 Status: v1.NodeStatus{ 1330 NodeInfo: v1.NodeSystemInfo{ 1331 KernelVersion: "KernelVersion", 1332 OSImage: "ContainerOSVersion", 1333 ContainerRuntimeVersion: "RuntimeType://RuntimeVersion", 1334 KubeletVersion: version.Get().String(), 1335 }, 1336 }, 1337 }, 1338 kubeProxyVersion: false, 1339 }, 1340 { 1341 desc: "DisableNodeKubeProxyVersion FeatureGate enable, KubeProxyVersion will be cleared if it is set.", 1342 node: &v1.Node{ 1343 Status: v1.NodeStatus{ 1344 NodeInfo: v1.NodeSystemInfo{ 1345 KernelVersion: "KernelVersion", 1346 OSImage: "ContainerOSVersion", 1347 ContainerRuntimeVersion: "RuntimeType://RuntimeVersion", 1348 KubeletVersion: version.Get().String(), 1349 KubeProxyVersion: version.Get().String(), 1350 }, 1351 }, 1352 }, 1353 versionInfo: &cadvisorapiv1.VersionInfo{ 1354 KernelVersion: "KernelVersion", 1355 ContainerOsVersion: "ContainerOSVersion", 1356 }, 1357 runtimeType: "RuntimeType", 1358 runtimeVersion: &kubecontainertest.FakeVersion{ 1359 Version: "RuntimeVersion", 1360 }, 1361 expectNode: &v1.Node{ 1362 Status: v1.NodeStatus{ 1363 NodeInfo: v1.NodeSystemInfo{ 1364 KernelVersion: "KernelVersion", 1365 OSImage: "ContainerOSVersion", 1366 ContainerRuntimeVersion: "RuntimeType://RuntimeVersion", 1367 KubeletVersion: version.Get().String(), 1368 }, 1369 }, 1370 }, 1371 kubeProxyVersion: false, 1372 }, 1373 } 1374 1375 for _, tc := range cases { 1376 t.Run(tc.desc, func(t *testing.T) { 1377 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DisableNodeKubeProxyVersion, !tc.kubeProxyVersion) 1378 1379 ctx := context.Background() 1380 versionInfoFunc := func() (*cadvisorapiv1.VersionInfo, error) { 1381 return tc.versionInfo, tc.versionInfoError 1382 } 1383 runtimeTypeFunc := func() string { 1384 return tc.runtimeType 1385 } 1386 runtimeVersionFunc := func(_ context.Context) (kubecontainer.Version, error) { 1387 return tc.runtimeVersion, tc.runtimeVersionError 1388 } 1389 // construct setter 1390 setter := VersionInfo(versionInfoFunc, runtimeTypeFunc, runtimeVersionFunc) 1391 // call setter on node 1392 err := setter(ctx, tc.node) 1393 require.Equal(t, tc.expectError, err) 1394 // check expected node 1395 assert.True(t, apiequality.Semantic.DeepEqual(tc.expectNode, tc.node), 1396 "Diff: %s", cmp.Diff(tc.expectNode, tc.node)) 1397 }) 1398 } 1399 } 1400 1401 func TestImages(t *testing.T) { 1402 const ( 1403 minImageSize = 23 * 1024 * 1024 1404 maxImageSize = 1000 * 1024 * 1024 1405 ) 1406 1407 cases := []struct { 1408 desc string 1409 maxImages int32 1410 imageList []kubecontainer.Image 1411 imageListError error 1412 expectError error 1413 }{ 1414 { 1415 desc: "max images enforced", 1416 maxImages: 1, 1417 imageList: makeImageList(2, 1, minImageSize, maxImageSize), 1418 }, 1419 { 1420 desc: "no max images cap for -1", 1421 maxImages: -1, 1422 imageList: makeImageList(2, 1, minImageSize, maxImageSize), 1423 }, 1424 { 1425 desc: "max names per image enforced", 1426 maxImages: -1, 1427 imageList: makeImageList(1, MaxNamesPerImageInNodeStatus+1, minImageSize, maxImageSize), 1428 }, 1429 { 1430 desc: "images are sorted by size, descending", 1431 maxImages: -1, 1432 // makeExpectedImageList will sort them for expectedNode when the test case is run 1433 imageList: []kubecontainer.Image{{Size: 3}, {Size: 1}, {Size: 4}, {Size: 2}}, 1434 }, 1435 { 1436 desc: "repo digests and tags both show up in image names", 1437 maxImages: -1, 1438 // makeExpectedImageList will use both digests and tags 1439 imageList: []kubecontainer.Image{ 1440 { 1441 RepoDigests: []string{"foo", "bar"}, 1442 RepoTags: []string{"baz", "quux"}, 1443 }, 1444 }, 1445 }, 1446 { 1447 desc: "error getting image list, image list on node is reset to empty", 1448 maxImages: -1, 1449 imageListError: fmt.Errorf("foo"), 1450 expectError: fmt.Errorf("error getting image list: foo"), 1451 }, 1452 } 1453 1454 for _, tc := range cases { 1455 t.Run(tc.desc, func(t *testing.T) { 1456 ctx := context.Background() 1457 imageListFunc := func() ([]kubecontainer.Image, error) { 1458 // today, imageListFunc is expected to return a sorted list, 1459 // but we may choose to sort in the setter at some future point 1460 // (e.g. if the image cache stopped sorting for us) 1461 sort.Sort(sliceutils.ByImageSize(tc.imageList)) 1462 return tc.imageList, tc.imageListError 1463 } 1464 // construct setter 1465 setter := Images(tc.maxImages, imageListFunc) 1466 // call setter on node 1467 node := &v1.Node{} 1468 err := setter(ctx, node) 1469 require.Equal(t, tc.expectError, err) 1470 // check expected node, image list should be reset to empty when there is an error 1471 expectNode := &v1.Node{} 1472 if err == nil { 1473 expectNode.Status.Images = makeExpectedImageList(tc.imageList, tc.maxImages, MaxNamesPerImageInNodeStatus) 1474 } 1475 assert.True(t, apiequality.Semantic.DeepEqual(expectNode, node), 1476 "Diff: %s", cmp.Diff(expectNode, node)) 1477 }) 1478 } 1479 1480 } 1481 1482 func TestReadyCondition(t *testing.T) { 1483 now := time.Now() 1484 before := now.Add(-time.Second) 1485 nowFunc := func() time.Time { return now } 1486 1487 withCapacity := &v1.Node{ 1488 Status: v1.NodeStatus{ 1489 Capacity: v1.ResourceList{ 1490 v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 1491 v1.ResourceMemory: *resource.NewQuantity(10e9, resource.BinarySI), 1492 v1.ResourcePods: *resource.NewQuantity(100, resource.DecimalSI), 1493 v1.ResourceEphemeralStorage: *resource.NewQuantity(5000, resource.BinarySI), 1494 }, 1495 }, 1496 } 1497 1498 withoutStorageCapacity := &v1.Node{ 1499 Status: v1.NodeStatus{ 1500 Capacity: v1.ResourceList{ 1501 v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 1502 v1.ResourceMemory: *resource.NewQuantity(10e9, resource.BinarySI), 1503 v1.ResourcePods: *resource.NewQuantity(100, resource.DecimalSI), 1504 }, 1505 }, 1506 } 1507 1508 cases := []struct { 1509 desc string 1510 node *v1.Node 1511 runtimeErrors error 1512 networkErrors error 1513 storageErrors error 1514 cmStatus cm.Status 1515 nodeShutdownManagerErrors error 1516 expectConditions []v1.NodeCondition 1517 expectEvents []testEvent 1518 disableLocalStorageCapacityIsolation bool 1519 }{ 1520 { 1521 desc: "new, ready", 1522 node: withCapacity.DeepCopy(), 1523 expectConditions: []v1.NodeCondition{*makeReadyCondition(true, "kubelet is posting ready status", now, now)}, 1524 // TODO(mtaufen): The current behavior is that we don't send an event for the initial NodeReady condition, 1525 // the reason for this is unclear, so we may want to actually send an event, and change these test cases 1526 // to ensure an event is sent. 1527 }, 1528 { 1529 desc: "new, ready: soft requirement warning", 1530 node: withCapacity.DeepCopy(), 1531 cmStatus: cm.Status{ 1532 SoftRequirements: fmt.Errorf("foo"), 1533 }, 1534 expectConditions: []v1.NodeCondition{*makeReadyCondition(true, "kubelet is posting ready status. WARNING: foo", now, now)}, 1535 }, 1536 { 1537 desc: "new, not ready: storage errors", 1538 node: withCapacity.DeepCopy(), 1539 storageErrors: errors.New("some storage error"), 1540 expectConditions: []v1.NodeCondition{*makeReadyCondition(false, "some storage error", now, now)}, 1541 }, 1542 { 1543 desc: "new, not ready: shutdown active", 1544 node: withCapacity.DeepCopy(), 1545 nodeShutdownManagerErrors: errors.New("node is shutting down"), 1546 expectConditions: []v1.NodeCondition{*makeReadyCondition(false, "node is shutting down", now, now)}, 1547 }, 1548 { 1549 desc: "new, not ready: runtime and network errors", 1550 node: withCapacity.DeepCopy(), 1551 runtimeErrors: errors.New("runtime"), 1552 networkErrors: errors.New("network"), 1553 expectConditions: []v1.NodeCondition{*makeReadyCondition(false, "[runtime, network]", now, now)}, 1554 }, 1555 { 1556 desc: "new, not ready: missing capacities", 1557 node: &v1.Node{}, 1558 expectConditions: []v1.NodeCondition{*makeReadyCondition(false, "missing node capacity for resources: cpu, memory, pods, ephemeral-storage", now, now)}, 1559 }, 1560 { 1561 desc: "new, ready: localStorageCapacityIsolation is not supported", 1562 node: withoutStorageCapacity.DeepCopy(), 1563 disableLocalStorageCapacityIsolation: true, 1564 expectConditions: []v1.NodeCondition{*makeReadyCondition(true, "kubelet is posting ready status", now, now)}, 1565 }, 1566 // the transition tests ensure timestamps are set correctly, no need to test the entire condition matrix in this section 1567 { 1568 desc: "transition to ready", 1569 node: func() *v1.Node { 1570 node := withCapacity.DeepCopy() 1571 node.Status.Conditions = []v1.NodeCondition{*makeReadyCondition(false, "", before, before)} 1572 return node 1573 }(), 1574 expectConditions: []v1.NodeCondition{*makeReadyCondition(true, "kubelet is posting ready status", now, now)}, 1575 expectEvents: []testEvent{ 1576 { 1577 eventType: v1.EventTypeNormal, 1578 event: events.NodeReady, 1579 }, 1580 }, 1581 }, 1582 { 1583 desc: "transition to not ready", 1584 node: func() *v1.Node { 1585 node := withCapacity.DeepCopy() 1586 node.Status.Conditions = []v1.NodeCondition{*makeReadyCondition(true, "", before, before)} 1587 return node 1588 }(), 1589 runtimeErrors: errors.New("foo"), 1590 expectConditions: []v1.NodeCondition{*makeReadyCondition(false, "foo", now, now)}, 1591 expectEvents: []testEvent{ 1592 { 1593 eventType: v1.EventTypeNormal, 1594 event: events.NodeNotReady, 1595 }, 1596 }, 1597 }, 1598 { 1599 desc: "ready, no transition", 1600 node: func() *v1.Node { 1601 node := withCapacity.DeepCopy() 1602 node.Status.Conditions = []v1.NodeCondition{*makeReadyCondition(true, "", before, before)} 1603 return node 1604 }(), 1605 expectConditions: []v1.NodeCondition{*makeReadyCondition(true, "kubelet is posting ready status", before, now)}, 1606 expectEvents: []testEvent{}, 1607 }, 1608 { 1609 desc: "not ready, no transition", 1610 node: func() *v1.Node { 1611 node := withCapacity.DeepCopy() 1612 node.Status.Conditions = []v1.NodeCondition{*makeReadyCondition(false, "", before, before)} 1613 return node 1614 }(), 1615 runtimeErrors: errors.New("foo"), 1616 expectConditions: []v1.NodeCondition{*makeReadyCondition(false, "foo", before, now)}, 1617 expectEvents: []testEvent{}, 1618 }, 1619 } 1620 for _, tc := range cases { 1621 t.Run(tc.desc, func(t *testing.T) { 1622 ctx := context.Background() 1623 runtimeErrorsFunc := func() error { 1624 return tc.runtimeErrors 1625 } 1626 networkErrorsFunc := func() error { 1627 return tc.networkErrors 1628 } 1629 storageErrorsFunc := func() error { 1630 return tc.storageErrors 1631 } 1632 cmStatusFunc := func() cm.Status { 1633 return tc.cmStatus 1634 } 1635 nodeShutdownErrorsFunc := func() error { 1636 return tc.nodeShutdownManagerErrors 1637 } 1638 events := []testEvent{} 1639 recordEventFunc := func(eventType, event string) { 1640 events = append(events, testEvent{ 1641 eventType: eventType, 1642 event: event, 1643 }) 1644 } 1645 // construct setter 1646 setter := ReadyCondition(nowFunc, runtimeErrorsFunc, networkErrorsFunc, storageErrorsFunc, cmStatusFunc, nodeShutdownErrorsFunc, recordEventFunc, !tc.disableLocalStorageCapacityIsolation) 1647 // call setter on node 1648 if err := setter(ctx, tc.node); err != nil { 1649 t.Fatalf("unexpected error: %v", err) 1650 } 1651 // check expected condition 1652 assert.True(t, apiequality.Semantic.DeepEqual(tc.expectConditions, tc.node.Status.Conditions), 1653 "Diff: %s", cmp.Diff(tc.expectConditions, tc.node.Status.Conditions)) 1654 // check expected events 1655 require.Equal(t, len(tc.expectEvents), len(events)) 1656 for i := range tc.expectEvents { 1657 assert.Equal(t, tc.expectEvents[i], events[i]) 1658 } 1659 }) 1660 } 1661 } 1662 1663 func TestMemoryPressureCondition(t *testing.T) { 1664 now := time.Now() 1665 before := now.Add(-time.Second) 1666 nowFunc := func() time.Time { return now } 1667 1668 cases := []struct { 1669 desc string 1670 node *v1.Node 1671 pressure bool 1672 expectConditions []v1.NodeCondition 1673 expectEvents []testEvent 1674 }{ 1675 { 1676 desc: "new, no pressure", 1677 node: &v1.Node{}, 1678 pressure: false, 1679 expectConditions: []v1.NodeCondition{*makeMemoryPressureCondition(false, now, now)}, 1680 expectEvents: []testEvent{ 1681 { 1682 eventType: v1.EventTypeNormal, 1683 event: "NodeHasSufficientMemory", 1684 }, 1685 }, 1686 }, 1687 { 1688 desc: "new, pressure", 1689 node: &v1.Node{}, 1690 pressure: true, 1691 expectConditions: []v1.NodeCondition{*makeMemoryPressureCondition(true, now, now)}, 1692 expectEvents: []testEvent{ 1693 { 1694 eventType: v1.EventTypeNormal, 1695 event: "NodeHasInsufficientMemory", 1696 }, 1697 }, 1698 }, 1699 { 1700 desc: "transition to pressure", 1701 node: &v1.Node{ 1702 Status: v1.NodeStatus{ 1703 Conditions: []v1.NodeCondition{*makeMemoryPressureCondition(false, before, before)}, 1704 }, 1705 }, 1706 pressure: true, 1707 expectConditions: []v1.NodeCondition{*makeMemoryPressureCondition(true, now, now)}, 1708 expectEvents: []testEvent{ 1709 { 1710 eventType: v1.EventTypeNormal, 1711 event: "NodeHasInsufficientMemory", 1712 }, 1713 }, 1714 }, 1715 { 1716 desc: "transition to no pressure", 1717 node: &v1.Node{ 1718 Status: v1.NodeStatus{ 1719 Conditions: []v1.NodeCondition{*makeMemoryPressureCondition(true, before, before)}, 1720 }, 1721 }, 1722 pressure: false, 1723 expectConditions: []v1.NodeCondition{*makeMemoryPressureCondition(false, now, now)}, 1724 expectEvents: []testEvent{ 1725 { 1726 eventType: v1.EventTypeNormal, 1727 event: "NodeHasSufficientMemory", 1728 }, 1729 }, 1730 }, 1731 { 1732 desc: "pressure, no transition", 1733 node: &v1.Node{ 1734 Status: v1.NodeStatus{ 1735 Conditions: []v1.NodeCondition{*makeMemoryPressureCondition(true, before, before)}, 1736 }, 1737 }, 1738 pressure: true, 1739 expectConditions: []v1.NodeCondition{*makeMemoryPressureCondition(true, before, now)}, 1740 expectEvents: []testEvent{}, 1741 }, 1742 { 1743 desc: "no pressure, no transition", 1744 node: &v1.Node{ 1745 Status: v1.NodeStatus{ 1746 Conditions: []v1.NodeCondition{*makeMemoryPressureCondition(false, before, before)}, 1747 }, 1748 }, 1749 pressure: false, 1750 expectConditions: []v1.NodeCondition{*makeMemoryPressureCondition(false, before, now)}, 1751 expectEvents: []testEvent{}, 1752 }, 1753 } 1754 for _, tc := range cases { 1755 t.Run(tc.desc, func(t *testing.T) { 1756 ctx := context.Background() 1757 events := []testEvent{} 1758 recordEventFunc := func(eventType, event string) { 1759 events = append(events, testEvent{ 1760 eventType: eventType, 1761 event: event, 1762 }) 1763 } 1764 pressureFunc := func() bool { 1765 return tc.pressure 1766 } 1767 // construct setter 1768 setter := MemoryPressureCondition(nowFunc, pressureFunc, recordEventFunc) 1769 // call setter on node 1770 if err := setter(ctx, tc.node); err != nil { 1771 t.Fatalf("unexpected error: %v", err) 1772 } 1773 // check expected condition 1774 assert.True(t, apiequality.Semantic.DeepEqual(tc.expectConditions, tc.node.Status.Conditions), 1775 "Diff: %s", cmp.Diff(tc.expectConditions, tc.node.Status.Conditions)) 1776 // check expected events 1777 require.Equal(t, len(tc.expectEvents), len(events)) 1778 for i := range tc.expectEvents { 1779 assert.Equal(t, tc.expectEvents[i], events[i]) 1780 } 1781 }) 1782 } 1783 } 1784 1785 func TestPIDPressureCondition(t *testing.T) { 1786 now := time.Now() 1787 before := now.Add(-time.Second) 1788 nowFunc := func() time.Time { return now } 1789 1790 cases := []struct { 1791 desc string 1792 node *v1.Node 1793 pressure bool 1794 expectConditions []v1.NodeCondition 1795 expectEvents []testEvent 1796 }{ 1797 { 1798 desc: "new, no pressure", 1799 node: &v1.Node{}, 1800 pressure: false, 1801 expectConditions: []v1.NodeCondition{*makePIDPressureCondition(false, now, now)}, 1802 expectEvents: []testEvent{ 1803 { 1804 eventType: v1.EventTypeNormal, 1805 event: "NodeHasSufficientPID", 1806 }, 1807 }, 1808 }, 1809 { 1810 desc: "new, pressure", 1811 node: &v1.Node{}, 1812 pressure: true, 1813 expectConditions: []v1.NodeCondition{*makePIDPressureCondition(true, now, now)}, 1814 expectEvents: []testEvent{ 1815 { 1816 eventType: v1.EventTypeNormal, 1817 event: "NodeHasInsufficientPID", 1818 }, 1819 }, 1820 }, 1821 { 1822 desc: "transition to pressure", 1823 node: &v1.Node{ 1824 Status: v1.NodeStatus{ 1825 Conditions: []v1.NodeCondition{*makePIDPressureCondition(false, before, before)}, 1826 }, 1827 }, 1828 pressure: true, 1829 expectConditions: []v1.NodeCondition{*makePIDPressureCondition(true, now, now)}, 1830 expectEvents: []testEvent{ 1831 { 1832 eventType: v1.EventTypeNormal, 1833 event: "NodeHasInsufficientPID", 1834 }, 1835 }, 1836 }, 1837 { 1838 desc: "transition to no pressure", 1839 node: &v1.Node{ 1840 Status: v1.NodeStatus{ 1841 Conditions: []v1.NodeCondition{*makePIDPressureCondition(true, before, before)}, 1842 }, 1843 }, 1844 pressure: false, 1845 expectConditions: []v1.NodeCondition{*makePIDPressureCondition(false, now, now)}, 1846 expectEvents: []testEvent{ 1847 { 1848 eventType: v1.EventTypeNormal, 1849 event: "NodeHasSufficientPID", 1850 }, 1851 }, 1852 }, 1853 { 1854 desc: "pressure, no transition", 1855 node: &v1.Node{ 1856 Status: v1.NodeStatus{ 1857 Conditions: []v1.NodeCondition{*makePIDPressureCondition(true, before, before)}, 1858 }, 1859 }, 1860 pressure: true, 1861 expectConditions: []v1.NodeCondition{*makePIDPressureCondition(true, before, now)}, 1862 expectEvents: []testEvent{}, 1863 }, 1864 { 1865 desc: "no pressure, no transition", 1866 node: &v1.Node{ 1867 Status: v1.NodeStatus{ 1868 Conditions: []v1.NodeCondition{*makePIDPressureCondition(false, before, before)}, 1869 }, 1870 }, 1871 pressure: false, 1872 expectConditions: []v1.NodeCondition{*makePIDPressureCondition(false, before, now)}, 1873 expectEvents: []testEvent{}, 1874 }, 1875 } 1876 for _, tc := range cases { 1877 t.Run(tc.desc, func(t *testing.T) { 1878 ctx := context.Background() 1879 events := []testEvent{} 1880 recordEventFunc := func(eventType, event string) { 1881 events = append(events, testEvent{ 1882 eventType: eventType, 1883 event: event, 1884 }) 1885 } 1886 pressureFunc := func() bool { 1887 return tc.pressure 1888 } 1889 // construct setter 1890 setter := PIDPressureCondition(nowFunc, pressureFunc, recordEventFunc) 1891 // call setter on node 1892 if err := setter(ctx, tc.node); err != nil { 1893 t.Fatalf("unexpected error: %v", err) 1894 } 1895 // check expected condition 1896 assert.True(t, apiequality.Semantic.DeepEqual(tc.expectConditions, tc.node.Status.Conditions), 1897 "Diff: %s", cmp.Diff(tc.expectConditions, tc.node.Status.Conditions)) 1898 // check expected events 1899 require.Equal(t, len(tc.expectEvents), len(events)) 1900 for i := range tc.expectEvents { 1901 assert.Equal(t, tc.expectEvents[i], events[i]) 1902 } 1903 }) 1904 } 1905 } 1906 1907 func TestDiskPressureCondition(t *testing.T) { 1908 now := time.Now() 1909 before := now.Add(-time.Second) 1910 nowFunc := func() time.Time { return now } 1911 1912 cases := []struct { 1913 desc string 1914 node *v1.Node 1915 pressure bool 1916 expectConditions []v1.NodeCondition 1917 expectEvents []testEvent 1918 }{ 1919 { 1920 desc: "new, no pressure", 1921 node: &v1.Node{}, 1922 pressure: false, 1923 expectConditions: []v1.NodeCondition{*makeDiskPressureCondition(false, now, now)}, 1924 expectEvents: []testEvent{ 1925 { 1926 eventType: v1.EventTypeNormal, 1927 event: "NodeHasNoDiskPressure", 1928 }, 1929 }, 1930 }, 1931 { 1932 desc: "new, pressure", 1933 node: &v1.Node{}, 1934 pressure: true, 1935 expectConditions: []v1.NodeCondition{*makeDiskPressureCondition(true, now, now)}, 1936 expectEvents: []testEvent{ 1937 { 1938 eventType: v1.EventTypeNormal, 1939 event: "NodeHasDiskPressure", 1940 }, 1941 }, 1942 }, 1943 { 1944 desc: "transition to pressure", 1945 node: &v1.Node{ 1946 Status: v1.NodeStatus{ 1947 Conditions: []v1.NodeCondition{*makeDiskPressureCondition(false, before, before)}, 1948 }, 1949 }, 1950 pressure: true, 1951 expectConditions: []v1.NodeCondition{*makeDiskPressureCondition(true, now, now)}, 1952 expectEvents: []testEvent{ 1953 { 1954 eventType: v1.EventTypeNormal, 1955 event: "NodeHasDiskPressure", 1956 }, 1957 }, 1958 }, 1959 { 1960 desc: "transition to no pressure", 1961 node: &v1.Node{ 1962 Status: v1.NodeStatus{ 1963 Conditions: []v1.NodeCondition{*makeDiskPressureCondition(true, before, before)}, 1964 }, 1965 }, 1966 pressure: false, 1967 expectConditions: []v1.NodeCondition{*makeDiskPressureCondition(false, now, now)}, 1968 expectEvents: []testEvent{ 1969 { 1970 eventType: v1.EventTypeNormal, 1971 event: "NodeHasNoDiskPressure", 1972 }, 1973 }, 1974 }, 1975 { 1976 desc: "pressure, no transition", 1977 node: &v1.Node{ 1978 Status: v1.NodeStatus{ 1979 Conditions: []v1.NodeCondition{*makeDiskPressureCondition(true, before, before)}, 1980 }, 1981 }, 1982 pressure: true, 1983 expectConditions: []v1.NodeCondition{*makeDiskPressureCondition(true, before, now)}, 1984 expectEvents: []testEvent{}, 1985 }, 1986 { 1987 desc: "no pressure, no transition", 1988 node: &v1.Node{ 1989 Status: v1.NodeStatus{ 1990 Conditions: []v1.NodeCondition{*makeDiskPressureCondition(false, before, before)}, 1991 }, 1992 }, 1993 pressure: false, 1994 expectConditions: []v1.NodeCondition{*makeDiskPressureCondition(false, before, now)}, 1995 expectEvents: []testEvent{}, 1996 }, 1997 } 1998 for _, tc := range cases { 1999 t.Run(tc.desc, func(t *testing.T) { 2000 ctx := context.Background() 2001 events := []testEvent{} 2002 recordEventFunc := func(eventType, event string) { 2003 events = append(events, testEvent{ 2004 eventType: eventType, 2005 event: event, 2006 }) 2007 } 2008 pressureFunc := func() bool { 2009 return tc.pressure 2010 } 2011 // construct setter 2012 setter := DiskPressureCondition(nowFunc, pressureFunc, recordEventFunc) 2013 // call setter on node 2014 if err := setter(ctx, tc.node); err != nil { 2015 t.Fatalf("unexpected error: %v", err) 2016 } 2017 // check expected condition 2018 assert.True(t, apiequality.Semantic.DeepEqual(tc.expectConditions, tc.node.Status.Conditions), 2019 "Diff: %s", cmp.Diff(tc.expectConditions, tc.node.Status.Conditions)) 2020 // check expected events 2021 require.Equal(t, len(tc.expectEvents), len(events)) 2022 for i := range tc.expectEvents { 2023 assert.Equal(t, tc.expectEvents[i], events[i]) 2024 } 2025 }) 2026 } 2027 } 2028 2029 func TestVolumesInUse(t *testing.T) { 2030 withVolumesInUse := &v1.Node{ 2031 Status: v1.NodeStatus{ 2032 VolumesInUse: []v1.UniqueVolumeName{"foo"}, 2033 }, 2034 } 2035 2036 cases := []struct { 2037 desc string 2038 node *v1.Node 2039 synced bool 2040 volumesInUse []v1.UniqueVolumeName 2041 expectVolumesInUse []v1.UniqueVolumeName 2042 }{ 2043 { 2044 desc: "synced", 2045 node: withVolumesInUse.DeepCopy(), 2046 synced: true, 2047 volumesInUse: []v1.UniqueVolumeName{"bar"}, 2048 expectVolumesInUse: []v1.UniqueVolumeName{"bar"}, 2049 }, 2050 { 2051 desc: "not synced", 2052 node: withVolumesInUse.DeepCopy(), 2053 synced: false, 2054 volumesInUse: []v1.UniqueVolumeName{"bar"}, 2055 expectVolumesInUse: []v1.UniqueVolumeName{"foo"}, 2056 }, 2057 } 2058 2059 for _, tc := range cases { 2060 t.Run(tc.desc, func(t *testing.T) { 2061 ctx := context.Background() 2062 syncedFunc := func() bool { 2063 return tc.synced 2064 } 2065 volumesInUseFunc := func() []v1.UniqueVolumeName { 2066 return tc.volumesInUse 2067 } 2068 // construct setter 2069 setter := VolumesInUse(syncedFunc, volumesInUseFunc) 2070 // call setter on node 2071 if err := setter(ctx, tc.node); err != nil { 2072 t.Fatalf("unexpected error: %v", err) 2073 } 2074 // check expected volumes 2075 assert.True(t, apiequality.Semantic.DeepEqual(tc.expectVolumesInUse, tc.node.Status.VolumesInUse), 2076 "Diff: %s", cmp.Diff(tc.expectVolumesInUse, tc.node.Status.VolumesInUse)) 2077 }) 2078 } 2079 } 2080 2081 func TestDaemonEndpoints(t *testing.T) { 2082 for _, test := range []struct { 2083 name string 2084 endpoints *v1.NodeDaemonEndpoints 2085 expected *v1.NodeDaemonEndpoints 2086 }{ 2087 { 2088 name: "empty daemon endpoints", 2089 endpoints: &v1.NodeDaemonEndpoints{}, 2090 expected: &v1.NodeDaemonEndpoints{KubeletEndpoint: v1.DaemonEndpoint{Port: 0}}, 2091 }, 2092 { 2093 name: "daemon endpoints with specific port", 2094 endpoints: &v1.NodeDaemonEndpoints{KubeletEndpoint: v1.DaemonEndpoint{Port: 5678}}, 2095 expected: &v1.NodeDaemonEndpoints{KubeletEndpoint: v1.DaemonEndpoint{Port: 5678}}, 2096 }, 2097 } { 2098 t.Run(test.name, func(t *testing.T) { 2099 ctx := context.Background() 2100 existingNode := &v1.Node{ 2101 ObjectMeta: metav1.ObjectMeta{ 2102 Name: testKubeletHostname, 2103 }, 2104 Spec: v1.NodeSpec{}, 2105 Status: v1.NodeStatus{ 2106 Addresses: []v1.NodeAddress{}, 2107 }, 2108 } 2109 2110 setter := DaemonEndpoints(test.endpoints) 2111 if err := setter(ctx, existingNode); err != nil { 2112 t.Fatal(err) 2113 } 2114 2115 assert.Equal(t, *test.expected, existingNode.Status.DaemonEndpoints) 2116 }) 2117 } 2118 } 2119 2120 // Test Helpers: 2121 2122 // testEvent is used to record events for tests 2123 type testEvent struct { 2124 eventType string 2125 event string 2126 message string 2127 } 2128 2129 // makeImageList randomly generates a list of images with the given count 2130 func makeImageList(numImages, numTags, minSize, maxSize int32) []kubecontainer.Image { 2131 images := make([]kubecontainer.Image, numImages) 2132 for i := range images { 2133 image := &images[i] 2134 image.ID = string(uuid.NewUUID()) 2135 image.RepoTags = makeImageTags(numTags) 2136 image.Size = rand.Int63nRange(int64(minSize), int64(maxSize+1)) 2137 } 2138 return images 2139 } 2140 2141 func makeExpectedImageList(imageList []kubecontainer.Image, maxImages, maxNames int32) []v1.ContainerImage { 2142 // copy the imageList, we do not want to mutate it in-place and accidentally edit a test case 2143 images := make([]kubecontainer.Image, len(imageList)) 2144 copy(images, imageList) 2145 // sort images by size 2146 sort.Sort(sliceutils.ByImageSize(images)) 2147 // convert to []v1.ContainerImage and truncate the list of names 2148 expectedImages := make([]v1.ContainerImage, len(images)) 2149 for i := range images { 2150 image := &images[i] 2151 expectedImage := &expectedImages[i] 2152 names := append(image.RepoDigests, image.RepoTags...) 2153 if len(names) > int(maxNames) { 2154 names = names[0:maxNames] 2155 } 2156 expectedImage.Names = names 2157 expectedImage.SizeBytes = image.Size 2158 } 2159 // -1 means no limit, truncate result list if necessary. 2160 if maxImages > -1 && 2161 int(maxImages) < len(expectedImages) { 2162 return expectedImages[0:maxImages] 2163 } 2164 return expectedImages 2165 } 2166 2167 func makeImageTags(num int32) []string { 2168 tags := make([]string, num) 2169 for i := range tags { 2170 tags[i] = "registry.k8s.io:v" + strconv.Itoa(i) 2171 } 2172 return tags 2173 } 2174 2175 func makeReadyCondition(ready bool, message string, transition, heartbeat time.Time) *v1.NodeCondition { 2176 if ready { 2177 return &v1.NodeCondition{ 2178 Type: v1.NodeReady, 2179 Status: v1.ConditionTrue, 2180 Reason: "KubeletReady", 2181 Message: message, 2182 LastTransitionTime: metav1.NewTime(transition), 2183 LastHeartbeatTime: metav1.NewTime(heartbeat), 2184 } 2185 } 2186 return &v1.NodeCondition{ 2187 Type: v1.NodeReady, 2188 Status: v1.ConditionFalse, 2189 Reason: "KubeletNotReady", 2190 Message: message, 2191 LastTransitionTime: metav1.NewTime(transition), 2192 LastHeartbeatTime: metav1.NewTime(heartbeat), 2193 } 2194 } 2195 2196 func makeMemoryPressureCondition(pressure bool, transition, heartbeat time.Time) *v1.NodeCondition { 2197 if pressure { 2198 return &v1.NodeCondition{ 2199 Type: v1.NodeMemoryPressure, 2200 Status: v1.ConditionTrue, 2201 Reason: "KubeletHasInsufficientMemory", 2202 Message: "kubelet has insufficient memory available", 2203 LastTransitionTime: metav1.NewTime(transition), 2204 LastHeartbeatTime: metav1.NewTime(heartbeat), 2205 } 2206 } 2207 return &v1.NodeCondition{ 2208 Type: v1.NodeMemoryPressure, 2209 Status: v1.ConditionFalse, 2210 Reason: "KubeletHasSufficientMemory", 2211 Message: "kubelet has sufficient memory available", 2212 LastTransitionTime: metav1.NewTime(transition), 2213 LastHeartbeatTime: metav1.NewTime(heartbeat), 2214 } 2215 } 2216 2217 func makePIDPressureCondition(pressure bool, transition, heartbeat time.Time) *v1.NodeCondition { 2218 if pressure { 2219 return &v1.NodeCondition{ 2220 Type: v1.NodePIDPressure, 2221 Status: v1.ConditionTrue, 2222 Reason: "KubeletHasInsufficientPID", 2223 Message: "kubelet has insufficient PID available", 2224 LastTransitionTime: metav1.NewTime(transition), 2225 LastHeartbeatTime: metav1.NewTime(heartbeat), 2226 } 2227 } 2228 return &v1.NodeCondition{ 2229 Type: v1.NodePIDPressure, 2230 Status: v1.ConditionFalse, 2231 Reason: "KubeletHasSufficientPID", 2232 Message: "kubelet has sufficient PID available", 2233 LastTransitionTime: metav1.NewTime(transition), 2234 LastHeartbeatTime: metav1.NewTime(heartbeat), 2235 } 2236 } 2237 2238 func makeDiskPressureCondition(pressure bool, transition, heartbeat time.Time) *v1.NodeCondition { 2239 if pressure { 2240 return &v1.NodeCondition{ 2241 Type: v1.NodeDiskPressure, 2242 Status: v1.ConditionTrue, 2243 Reason: "KubeletHasDiskPressure", 2244 Message: "kubelet has disk pressure", 2245 LastTransitionTime: metav1.NewTime(transition), 2246 LastHeartbeatTime: metav1.NewTime(heartbeat), 2247 } 2248 } 2249 return &v1.NodeCondition{ 2250 Type: v1.NodeDiskPressure, 2251 Status: v1.ConditionFalse, 2252 Reason: "KubeletHasNoDiskPressure", 2253 Message: "kubelet has no disk pressure", 2254 LastTransitionTime: metav1.NewTime(transition), 2255 LastHeartbeatTime: metav1.NewTime(heartbeat), 2256 } 2257 }