github.com/kotalco/kotal@v0.3.0/apis/ethereum/v1alpha1/node_validation_webhook_test.go (about) 1 package v1alpha1 2 3 import ( 4 "fmt" 5 6 "github.com/kotalco/kotal/apis/shared" 7 . "github.com/onsi/ginkgo/v2" 8 . "github.com/onsi/gomega" 9 "k8s.io/apimachinery/pkg/api/errors" 10 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 "k8s.io/apimachinery/pkg/util/validation/field" 12 ) 13 14 var _ = Describe("Ethereum node validation", func() { 15 16 var ( 17 networkID uint = 77777 18 fixedDifficulty uint = 1500 19 coinbase = shared.EthereumAddress("0xd2c21213027cbf4d46c16b55fa98e5252b048706") 20 ) 21 22 createCases := []struct { 23 Title string 24 Node *Node 25 Errors field.ErrorList 26 }{ 27 { 28 Title: "node #2", 29 Node: &Node{ 30 ObjectMeta: metav1.ObjectMeta{ 31 Name: "node-1", 32 }, 33 Spec: NodeSpec{ 34 Genesis: &Genesis{ 35 ChainID: 444, 36 }, 37 Client: BesuClient, 38 Network: GoerliNetwork, 39 }, 40 }, 41 Errors: field.ErrorList{ 42 { 43 Type: field.ErrorTypeInvalid, 44 Field: "spec.network", 45 BadValue: GoerliNetwork, 46 Detail: "must be none if spec.genesis is specified", 47 }, 48 }, 49 }, 50 { 51 Title: "node #3", 52 Node: &Node{ 53 ObjectMeta: metav1.ObjectMeta{ 54 Name: "node-1", 55 }, 56 Spec: NodeSpec{ 57 Client: BesuClient, 58 }, 59 }, 60 Errors: field.ErrorList{ 61 { 62 Type: field.ErrorTypeInvalid, 63 Field: "spec.genesis", 64 BadValue: "", 65 Detail: "must be specified if spec.network is none", 66 }, 67 }, 68 }, 69 { 70 Title: "node #10", 71 Node: &Node{ 72 ObjectMeta: metav1.ObjectMeta{ 73 Name: "node-1", 74 }, 75 Spec: NodeSpec{ 76 Genesis: &Genesis{ 77 ChainID: 55555, 78 }, 79 Miner: true, 80 Client: BesuClient, 81 }, 82 }, 83 Errors: field.ErrorList{ 84 { 85 Type: field.ErrorTypeInvalid, 86 Field: "spec.coinbase", 87 BadValue: "", 88 Detail: "must provide coinbase if miner is true", 89 }, 90 }, 91 }, 92 { 93 Title: "node #10", 94 Node: &Node{ 95 ObjectMeta: metav1.ObjectMeta{ 96 Name: "node-1", 97 }, 98 Spec: NodeSpec{ 99 Client: BesuClient, 100 Network: GoerliNetwork, 101 Engine: true, 102 }, 103 }, 104 Errors: field.ErrorList{ 105 { 106 Type: field.ErrorTypeInvalid, 107 Field: "spec.jwtSecretName", 108 BadValue: "", 109 Detail: "must provide jwtSecretName if engine is true", 110 }, 111 }, 112 }, 113 { 114 Title: "node #11", 115 Node: &Node{ 116 ObjectMeta: metav1.ObjectMeta{ 117 Name: "node-1", 118 }, 119 Spec: NodeSpec{ 120 Genesis: &Genesis{ 121 ChainID: 55555, 122 IBFT2: &IBFT2{}, 123 }, 124 Coinbase: shared.EthereumAddress("0x676aEda2E67D24eb304cFf75A5190824831E3399"), 125 Client: BesuClient, 126 }, 127 }, 128 Errors: field.ErrorList{ 129 { 130 Type: field.ErrorTypeInvalid, 131 Field: "spec.miner", 132 BadValue: false, 133 Detail: "must set miner to true if coinbase is provided", 134 }, 135 }, 136 }, 137 { 138 Title: "node #16", 139 Node: &Node{ 140 ObjectMeta: metav1.ObjectMeta{ 141 Name: "node-1", 142 }, 143 Spec: NodeSpec{ 144 Genesis: &Genesis{ 145 ChainID: 55555, 146 NetworkID: networkID, 147 Ethash: &Ethash{}, 148 }, 149 Client: GethClient, 150 Miner: true, 151 Coinbase: coinbase, 152 }, 153 }, 154 Errors: field.ErrorList{ 155 { 156 Type: field.ErrorTypeInvalid, 157 Field: "spec.import", 158 BadValue: "", 159 Detail: "must import coinbase account", 160 }, 161 }, 162 }, 163 { 164 Title: "node #18", 165 Node: &Node{ 166 ObjectMeta: metav1.ObjectMeta{ 167 Name: "node-1", 168 }, 169 Spec: NodeSpec{ 170 Genesis: &Genesis{ 171 ChainID: 55555, 172 NetworkID: networkID, 173 Ethash: &Ethash{}, 174 }, 175 Client: BesuClient, 176 Miner: true, 177 Coinbase: coinbase, 178 Import: &ImportedAccount{ 179 PrivateKeySecretName: "my-account-privatekey", 180 PasswordSecretName: "my-account-password", 181 }, 182 }, 183 }, 184 Errors: field.ErrorList{ 185 { 186 Type: field.ErrorTypeInvalid, 187 Field: "spec.client", 188 BadValue: "besu", 189 Detail: "client doesn't support importing accounts", 190 }, 191 }, 192 }, 193 { 194 Title: "node #19", 195 Node: &Node{ 196 ObjectMeta: metav1.ObjectMeta{ 197 Name: "node-1", 198 }, 199 Spec: NodeSpec{ 200 Genesis: &Genesis{ 201 NetworkID: networkID, 202 ChainID: 55555, 203 IBFT2: &IBFT2{}, 204 }, 205 Client: GethClient, 206 }, 207 }, 208 Errors: field.ErrorList{ 209 { 210 Type: field.ErrorTypeInvalid, 211 Field: "spec.client", 212 BadValue: "geth", 213 Detail: "client doesn't support ibft2 consensus", 214 }, 215 }, 216 }, 217 { 218 Title: "node #20", 219 Node: &Node{ 220 ObjectMeta: metav1.ObjectMeta{ 221 Name: "node-1", 222 }, 223 Spec: NodeSpec{ 224 Genesis: &Genesis{ 225 ChainID: 55555, 226 NetworkID: networkID, 227 Clique: &Clique{}, 228 }, 229 Client: GethClient, 230 RPC: true, 231 Miner: true, 232 Coinbase: coinbase, 233 Import: &ImportedAccount{ 234 PrivateKeySecretName: "my-account-privatekey", 235 PasswordSecretName: "my-account-password", 236 }, 237 }, 238 }, 239 Errors: field.ErrorList{ 240 { 241 Type: field.ErrorTypeInvalid, 242 Field: "spec.rpc", 243 BadValue: true, 244 Detail: "must be false if import is provided", 245 }, 246 }, 247 }, 248 { 249 Title: "node #21", 250 Node: &Node{ 251 ObjectMeta: metav1.ObjectMeta{ 252 Name: "node-1", 253 }, 254 Spec: NodeSpec{ 255 Genesis: &Genesis{ 256 ChainID: 55555, 257 NetworkID: networkID, 258 Clique: &Clique{}, 259 }, 260 Client: GethClient, 261 WS: true, 262 Miner: true, 263 Coinbase: coinbase, 264 Import: &ImportedAccount{ 265 PrivateKeySecretName: "my-account-privatekey", 266 PasswordSecretName: "my-account-password", 267 }, 268 }, 269 }, 270 Errors: field.ErrorList{ 271 { 272 Type: field.ErrorTypeInvalid, 273 Field: "spec.ws", 274 BadValue: true, 275 Detail: "must be false if import is provided", 276 }, 277 }, 278 }, 279 { 280 Title: "node #22", 281 Node: &Node{ 282 ObjectMeta: metav1.ObjectMeta{ 283 Name: "node-1", 284 }, 285 Spec: NodeSpec{ 286 Genesis: &Genesis{ 287 ChainID: 55555, 288 NetworkID: networkID, 289 Clique: &Clique{}, 290 }, 291 Client: GethClient, 292 GraphQL: true, 293 Miner: true, 294 Coinbase: coinbase, 295 Import: &ImportedAccount{ 296 PrivateKeySecretName: "my-account-privatekey", 297 PasswordSecretName: "my-account-password", 298 }, 299 }, 300 }, 301 Errors: field.ErrorList{ 302 { 303 Type: field.ErrorTypeInvalid, 304 Field: "spec.graphql", 305 BadValue: true, 306 Detail: "must be false if import is provided", 307 }, 308 }, 309 }, 310 { 311 Title: "node #23", 312 Node: &Node{ 313 ObjectMeta: metav1.ObjectMeta{ 314 Name: "node-1", 315 }, 316 Spec: NodeSpec{ 317 Genesis: &Genesis{ 318 ChainID: 55555, 319 NetworkID: networkID, 320 Ethash: &Ethash{ 321 FixedDifficulty: &fixedDifficulty, 322 }, 323 }, 324 Client: GethClient, 325 }, 326 }, 327 Errors: field.ErrorList{ 328 { 329 Type: field.ErrorTypeInvalid, 330 Field: "spec.client", 331 BadValue: "geth", 332 Detail: "client doesn't support fixed difficulty pow networks", 333 }, 334 }, 335 }, 336 { 337 Title: "node #24", 338 Node: &Node{ 339 ObjectMeta: metav1.ObjectMeta{ 340 Name: "node-1", 341 }, 342 Spec: NodeSpec{ 343 Client: BesuClient, 344 Network: GoerliNetwork, 345 SyncMode: LightSynchronization, 346 }, 347 }, 348 Errors: field.ErrorList{ 349 { 350 Type: field.ErrorTypeInvalid, 351 Field: "spec.syncMode", 352 BadValue: LightSynchronization, 353 Detail: "not supported by client besu", 354 }, 355 }, 356 }, 357 { 358 Title: "node #24", 359 Node: &Node{ 360 ObjectMeta: metav1.ObjectMeta{ 361 Name: "node-1", 362 }, 363 Spec: NodeSpec{ 364 Client: NethermindClient, 365 Network: GoerliNetwork, 366 SyncMode: SnapSynchronization, 367 }, 368 }, 369 Errors: field.ErrorList{ 370 { 371 Type: field.ErrorTypeInvalid, 372 Field: "spec.syncMode", 373 BadValue: SnapSynchronization, 374 Detail: "not supported by client nethermind", 375 }, 376 }, 377 }, 378 { 379 Title: "node #25", 380 Node: &Node{ 381 ObjectMeta: metav1.ObjectMeta{ 382 Name: "node-1", 383 }, 384 Spec: NodeSpec{ 385 Client: BesuClient, 386 Network: GoerliNetwork, 387 Resources: shared.Resources{ 388 CPU: "2", 389 CPULimit: "1", 390 }, 391 }, 392 }, 393 Errors: field.ErrorList{ 394 { 395 Type: field.ErrorTypeInvalid, 396 Field: "spec.resources.cpuLimit", 397 BadValue: "1", 398 Detail: "must be greater than or equal to cpu 2", 399 }, 400 }, 401 }, 402 { 403 Title: "node #26", 404 Node: &Node{ 405 ObjectMeta: metav1.ObjectMeta{ 406 Name: "node-1", 407 }, 408 Spec: NodeSpec{ 409 Client: BesuClient, 410 Network: GoerliNetwork, 411 Resources: shared.Resources{ 412 CPU: "1", 413 CPULimit: "2", 414 Memory: "2Gi", 415 MemoryLimit: "1Gi", 416 }, 417 }, 418 }, 419 Errors: field.ErrorList{ 420 { 421 Type: field.ErrorTypeInvalid, 422 Field: "spec.resources.memoryLimit", 423 BadValue: "1Gi", 424 Detail: "must be greater than memory 2Gi", 425 }, 426 }, 427 }, 428 { 429 Title: "node #28", 430 Node: &Node{ 431 ObjectMeta: metav1.ObjectMeta{ 432 Name: "node-1", 433 }, 434 Spec: NodeSpec{ 435 Client: GethClient, 436 Network: GoerliNetwork, 437 Logging: shared.FatalLogs, 438 }, 439 }, 440 Errors: field.ErrorList{ 441 { 442 Type: field.ErrorTypeInvalid, 443 Field: "spec.logging", 444 BadValue: shared.FatalLogs, 445 Detail: "not supported by client geth", 446 }, 447 }, 448 }, 449 { 450 Title: "node #29", 451 Node: &Node{ 452 ObjectMeta: metav1.ObjectMeta{ 453 Name: "node-1", 454 }, 455 Spec: NodeSpec{ 456 Client: GethClient, 457 Network: GoerliNetwork, 458 Logging: shared.TraceLogs, 459 }, 460 }, 461 Errors: field.ErrorList{ 462 { 463 Type: field.ErrorTypeInvalid, 464 Field: "spec.logging", 465 BadValue: shared.TraceLogs, 466 Detail: "not supported by client geth", 467 }, 468 }, 469 }, 470 { 471 Title: "node #37", 472 Node: &Node{ 473 ObjectMeta: metav1.ObjectMeta{ 474 Name: "node-1", 475 }, 476 Spec: NodeSpec{ 477 Client: GethClient, 478 Network: GoerliNetwork, 479 GraphQL: true, 480 }, 481 }, 482 Errors: field.ErrorList{ 483 { 484 Type: field.ErrorTypeInvalid, 485 Field: "spec.rpc", 486 BadValue: false, 487 Detail: "must enable rpc if client is geth and graphql is enabled", 488 }, 489 }, 490 }, 491 { 492 Title: "node #38", 493 Node: &Node{ 494 ObjectMeta: metav1.ObjectMeta{ 495 Name: "node-1", 496 }, 497 Spec: NodeSpec{ 498 Client: NethermindClient, 499 Network: GoerliNetwork, 500 Hosts: []string{"kotal.com"}, 501 }, 502 }, 503 Errors: field.ErrorList{ 504 { 505 Type: field.ErrorTypeInvalid, 506 Field: "spec.client", 507 BadValue: NethermindClient, 508 Detail: "client doesn't support hosts whitelisting", 509 }, 510 }, 511 }, 512 { 513 Title: "node #39", 514 Node: &Node{ 515 ObjectMeta: metav1.ObjectMeta{ 516 Name: "node-1", 517 }, 518 Spec: NodeSpec{ 519 Client: NethermindClient, 520 Network: GoerliNetwork, 521 CORSDomains: []string{"kotal.com"}, 522 }, 523 }, 524 Errors: field.ErrorList{ 525 { 526 Type: field.ErrorTypeInvalid, 527 Field: "spec.client", 528 BadValue: NethermindClient, 529 Detail: "client doesn't support CORS domains", 530 }, 531 }, 532 }, 533 } 534 535 // TODO: move .resources validation to shared resources package 536 updateCases := []struct { 537 Title string 538 OldNode *Node 539 NewNode *Node 540 Errors field.ErrorList 541 }{ 542 { 543 Title: "node #1", 544 OldNode: &Node{ 545 ObjectMeta: metav1.ObjectMeta{ 546 Name: "node-1", 547 }, 548 Spec: NodeSpec{ 549 Client: BesuClient, 550 Network: GoerliNetwork, 551 }, 552 }, 553 NewNode: &Node{ 554 ObjectMeta: metav1.ObjectMeta{ 555 Name: "node-1", 556 }, 557 Spec: NodeSpec{ 558 Client: BesuClient, 559 Network: MainNetwork, 560 }, 561 }, 562 Errors: field.ErrorList{ 563 { 564 Type: field.ErrorTypeInvalid, 565 Field: "spec.network", 566 BadValue: MainNetwork, 567 Detail: "field is immutable", 568 }, 569 }, 570 }, 571 { 572 Title: "node #2", 573 OldNode: &Node{ 574 ObjectMeta: metav1.ObjectMeta{ 575 Name: "node-2", 576 }, 577 Spec: NodeSpec{ 578 Client: BesuClient, 579 Network: GoerliNetwork, 580 Resources: shared.Resources{ 581 Storage: "20Gi", 582 }, 583 }, 584 }, 585 NewNode: &Node{ 586 ObjectMeta: metav1.ObjectMeta{ 587 Name: "node-2", 588 }, 589 Spec: NodeSpec{ 590 Client: BesuClient, 591 Network: GoerliNetwork, 592 Resources: shared.Resources{ 593 Storage: "10Gi", 594 }, 595 }, 596 }, 597 Errors: field.ErrorList{ 598 { 599 Type: field.ErrorTypeInvalid, 600 Field: "spec.resources.storage", 601 BadValue: "10Gi", 602 Detail: "must be greater than or equal to old storage 20Gi", 603 }, 604 }, 605 }, 606 { 607 Title: "node #3", 608 OldNode: &Node{ 609 ObjectMeta: metav1.ObjectMeta{ 610 Name: "node-3", 611 }, 612 Spec: NodeSpec{ 613 Client: BesuClient, 614 Network: GoerliNetwork, 615 Resources: shared.Resources{ 616 CPU: "1", 617 CPULimit: "2", 618 }, 619 }, 620 }, 621 NewNode: &Node{ 622 ObjectMeta: metav1.ObjectMeta{ 623 Name: "node-3", 624 }, 625 Spec: NodeSpec{ 626 Client: BesuClient, 627 Network: GoerliNetwork, 628 Resources: shared.Resources{ 629 CPU: "2", 630 CPULimit: "1", 631 }, 632 }, 633 }, 634 Errors: field.ErrorList{ 635 { 636 Type: field.ErrorTypeInvalid, 637 Field: "spec.resources.cpuLimit", 638 BadValue: "1", 639 Detail: "must be greater than or equal to cpu 2", 640 }, 641 }, 642 }, 643 { 644 Title: "node #4", 645 OldNode: &Node{ 646 ObjectMeta: metav1.ObjectMeta{ 647 Name: "node-4", 648 }, 649 Spec: NodeSpec{ 650 Client: BesuClient, 651 Network: GoerliNetwork, 652 Resources: shared.Resources{ 653 Memory: "1Gi", 654 MemoryLimit: "2Gi", 655 }, 656 }, 657 }, 658 NewNode: &Node{ 659 ObjectMeta: metav1.ObjectMeta{ 660 Name: "node-4", 661 }, 662 Spec: NodeSpec{ 663 Client: BesuClient, 664 Network: GoerliNetwork, 665 Resources: shared.Resources{ 666 Memory: "1Gi", 667 MemoryLimit: "1Gi", 668 }, 669 }, 670 }, 671 Errors: field.ErrorList{ 672 { 673 Type: field.ErrorTypeInvalid, 674 Field: "spec.resources.memoryLimit", 675 BadValue: "1Gi", 676 Detail: "must be greater than memory 1Gi", 677 }, 678 }, 679 }, 680 { 681 Title: "node #5", 682 OldNode: &Node{ 683 ObjectMeta: metav1.ObjectMeta{ 684 Name: "node-5", 685 }, 686 Spec: NodeSpec{ 687 Client: BesuClient, 688 Network: GoerliNetwork, 689 }, 690 }, 691 NewNode: &Node{ 692 ObjectMeta: metav1.ObjectMeta{ 693 Name: "node-5", 694 }, 695 Spec: NodeSpec{ 696 Client: GethClient, 697 Network: GoerliNetwork, 698 }, 699 }, 700 Errors: field.ErrorList{ 701 { 702 Type: field.ErrorTypeInvalid, 703 Field: "spec.client", 704 BadValue: GethClient, 705 Detail: "field is immutable", 706 }, 707 }, 708 }, 709 } 710 711 Context("While creating node", func() { 712 for _, c := range createCases { 713 func() { 714 cc := c 715 It(fmt.Sprintf("Should validate %s", cc.Title), func() { 716 cc.Node.Default() 717 _, err := cc.Node.ValidateCreate() 718 719 errStatus := err.(*errors.StatusError) 720 721 causes := shared.ErrorsToCauses(cc.Errors) 722 723 Expect(errStatus.ErrStatus.Details.Causes).To(ContainElements(causes)) 724 }) 725 }() 726 } 727 }) 728 729 Context("While updating node", func() { 730 for _, c := range updateCases { 731 func() { 732 cc := c 733 It(fmt.Sprintf("Should validate %s", cc.Title), func() { 734 cc.OldNode.Default() 735 cc.NewNode.Default() 736 _, err := cc.NewNode.ValidateUpdate(cc.OldNode) 737 738 errStatus := err.(*errors.StatusError) 739 740 causes := shared.ErrorsToCauses(cc.Errors) 741 742 Expect(errStatus.ErrStatus.Details.Causes).To(ContainElements(causes)) 743 }) 744 }() 745 } 746 }) 747 748 })