github.com/kotalco/kotal@v0.3.0/controllers/ethereum/node_controller.go (about) 1 package controllers 2 3 import ( 4 "context" 5 _ "embed" 6 "fmt" 7 "strings" 8 9 appsv1 "k8s.io/api/apps/v1" 10 corev1 "k8s.io/api/core/v1" 11 "k8s.io/apimachinery/pkg/api/resource" 12 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 "k8s.io/apimachinery/pkg/runtime" 14 "k8s.io/apimachinery/pkg/types" 15 "k8s.io/apimachinery/pkg/util/intstr" 16 ctrl "sigs.k8s.io/controller-runtime" 17 "sigs.k8s.io/controller-runtime/pkg/client" 18 "sigs.k8s.io/controller-runtime/pkg/log" 19 "sigs.k8s.io/controller-runtime/pkg/predicate" 20 21 ethereumv1alpha1 "github.com/kotalco/kotal/apis/ethereum/v1alpha1" 22 ethereumClients "github.com/kotalco/kotal/clients/ethereum" 23 "github.com/kotalco/kotal/controllers/shared" 24 "github.com/kotalco/kotal/helpers" 25 ) 26 27 // NodeReconciler reconciles a Node object 28 type NodeReconciler struct { 29 client.Client 30 Scheme *runtime.Scheme 31 } 32 33 const ( 34 envCoinbase = "KOTAL_COINBASE" 35 ) 36 37 var ( 38 //go:embed geth_init_genesis.sh 39 GethInitGenesisScript string 40 //go:embed geth_import_account.sh 41 gethImportAccountScript string 42 //go:embed nethermind_convert_enode_privatekey.sh 43 nethermindConvertEnodePrivateKeyScript string 44 //go:embed nethermind_copy_keystore.sh 45 nethermindConvertCopyKeystoreScript string 46 ) 47 48 // +kubebuilder:rbac:groups=ethereum.kotal.io,resources=nodes,verbs=get;list;watch;create;update;patch;delete 49 // +kubebuilder:rbac:groups=ethereum.kotal.io,resources=nodes/status,verbs=get;update;patch 50 // +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=watch;get;list;create;update;delete 51 // +kubebuilder:rbac:groups=core,resources=secrets;services;configmaps;persistentvolumeclaims,verbs=watch;get;create;update;list;delete 52 53 // Reconcile reconciles ethereum networks 54 func (r *NodeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { 55 defer shared.IgnoreConflicts(&err) 56 57 var node ethereumv1alpha1.Node 58 59 if err = r.Client.Get(ctx, req.NamespacedName, &node); err != nil { 60 err = client.IgnoreNotFound(err) 61 return 62 } 63 64 // default the node if webhooks are disabled 65 if !shared.IsWebhookEnabled() { 66 node.Default() 67 } 68 69 shared.UpdateLabels(&node, string(node.Spec.Client), node.Spec.Network) 70 r.updateStaticNodes(ctx, &node) 71 r.updateBootnodes(ctx, &node) 72 73 if err = r.reconcilePVC(ctx, &node); err != nil { 74 return 75 } 76 77 if err = r.reconcileConfigmap(ctx, &node); err != nil { 78 return 79 } 80 81 ip, err := r.reconcileService(ctx, &node) 82 if err != nil { 83 return 84 } 85 86 if err = r.reconcileStatefulSet(ctx, &node); err != nil { 87 return 88 } 89 90 var publicKey string 91 if publicKey, err = r.reconcileSecret(ctx, &node); err != nil { 92 return 93 } 94 95 enodeURL := fmt.Sprintf("enode://%s@%s:%d", publicKey, ip, node.Spec.P2PPort) 96 97 if err = r.updateStatus(ctx, &node, enodeURL); err != nil { 98 return 99 } 100 101 return ctrl.Result{}, nil 102 } 103 104 // getEnodeURL fetch enodeURL from enode that has the format of node.namespace 105 // name is the node name, and namespace is the node namespace 106 func (r *NodeReconciler) getEnodeURL(ctx context.Context, enode, ns string) (string, error) { 107 node := ðereumv1alpha1.Node{} 108 var name, namespace string 109 110 if parts := strings.Split(enode, "."); len(parts) > 1 { 111 name = parts[0] 112 namespace = parts[1] 113 } else { 114 // nodes without . refered to nodes in the current node namespace 115 name = enode 116 namespace = ns 117 } 118 119 namespacedName := types.NamespacedName{ 120 Name: name, 121 Namespace: namespace, 122 } 123 124 if err := r.Client.Get(ctx, namespacedName, node); err != nil { 125 return "", err 126 } 127 128 return node.Status.EnodeURL, nil 129 } 130 131 // updateStaticNodes replaces Ethereum node references with their enodeURL 132 func (r *NodeReconciler) updateStaticNodes(ctx context.Context, node *ethereumv1alpha1.Node) { 133 log := log.FromContext(ctx) 134 for i, enode := range node.Spec.StaticNodes { 135 if !strings.HasPrefix(string(enode), "enode://") { 136 enodeURL, err := r.getEnodeURL(ctx, string(enode), node.Namespace) 137 if err != nil { 138 // remove static node reference, so it won't be included into static nodes file 139 // don't return the error, node maybe not up and running yet 140 node.Spec.StaticNodes = append(node.Spec.StaticNodes[:i], node.Spec.StaticNodes[i+1:]...) 141 log.Error(err, "failed to get static node") 142 continue 143 } 144 log.Info("static node enodeURL", string(enode), enodeURL) 145 // replace reference with actual enode url 146 if strings.HasPrefix(enodeURL, "enode://") { 147 node.Spec.StaticNodes[i] = ethereumv1alpha1.Enode(enodeURL) 148 } else { 149 // remove static node reference, so it won't be included into static nodes file 150 node.Spec.StaticNodes = append(node.Spec.StaticNodes[:i], node.Spec.StaticNodes[i+1:]...) 151 } 152 } 153 } 154 } 155 156 // updateBootnodes replaces Ethereum node references with their enodeURL 157 func (r *NodeReconciler) updateBootnodes(ctx context.Context, node *ethereumv1alpha1.Node) { 158 log := log.FromContext(ctx) 159 for i, enode := range node.Spec.Bootnodes { 160 if !strings.HasPrefix(string(enode), "enode://") { 161 enodeURL, err := r.getEnodeURL(ctx, string(enode), node.Namespace) 162 if err != nil { 163 // remove bootnode reference, so it won't be included into bootnodes 164 // don't return the error, node maybe not up and running yet 165 node.Spec.Bootnodes = append(node.Spec.Bootnodes[:i], node.Spec.Bootnodes[i+1:]...) 166 log.Error(err, "failed to get bootnode") 167 continue 168 } 169 log.Info("bootnode enodeURL", string(enode), enodeURL) 170 // replace reference with actual enode url 171 if strings.HasPrefix(enodeURL, "enode://") { 172 node.Spec.Bootnodes[i] = ethereumv1alpha1.Enode(enodeURL) 173 } else { 174 // remove bootnode reference, so it won't be included into bootnodes 175 node.Spec.Bootnodes = append(node.Spec.Bootnodes[:i], node.Spec.Bootnodes[i+1:]...) 176 } 177 } 178 } 179 } 180 181 // updateStatus updates network status 182 func (r *NodeReconciler) updateStatus(ctx context.Context, node *ethereumv1alpha1.Node, enodeURL string) error { 183 var consensus, network string 184 185 log := log.FromContext(ctx) 186 187 if node.Spec.Genesis == nil { 188 switch node.Spec.Network { 189 case ethereumv1alpha1.MainNetwork, 190 ethereumv1alpha1.RopstenNetwork, 191 ethereumv1alpha1.XDaiNetwork, 192 ethereumv1alpha1.GoerliNetwork: 193 consensus = "pos" 194 case ethereumv1alpha1.RinkebyNetwork: 195 consensus = "poa" 196 } 197 } else { 198 if node.Spec.Genesis.Ethash != nil { 199 consensus = "pow" 200 } else if node.Spec.Genesis.Clique != nil { 201 consensus = "poa" 202 } else if node.Spec.Genesis.IBFT2 != nil { 203 consensus = "ibft2" 204 } 205 } 206 207 node.Status.Consensus = consensus 208 209 if network = node.Spec.Network; network == "" { 210 network = "private" 211 } 212 213 node.Status.Network = network 214 215 if node.Spec.NodePrivateKeySecretName == "" { 216 switch node.Spec.Client { 217 case ethereumv1alpha1.BesuClient: 218 enodeURL = "call net_enode JSON-RPC method" 219 case ethereumv1alpha1.GethClient: 220 enodeURL = "call admin_nodeInfo JSON-RPC method" 221 case ethereumv1alpha1.NethermindClient: 222 enodeURL = "call net_localEnode JSON-RPC method" 223 } 224 } 225 226 node.Status.EnodeURL = enodeURL 227 228 if err := r.Status().Update(ctx, node); err != nil { 229 log.Error(err, "unable to update node status") 230 return err 231 } 232 233 return nil 234 } 235 236 // specConfigmap updates genesis configmap spec 237 func (r *NodeReconciler) specConfigmap(node *ethereumv1alpha1.Node, configmap *corev1.ConfigMap, genesis, staticNodes string) { 238 if configmap.Data == nil { 239 configmap.Data = map[string]string{} 240 } 241 242 var key, importAccountScript string 243 244 switch node.Spec.Client { 245 case ethereumv1alpha1.GethClient: 246 key = "config.toml" 247 importAccountScript = gethImportAccountScript 248 case ethereumv1alpha1.BesuClient: 249 key = "static-nodes.json" 250 case ethereumv1alpha1.NethermindClient: 251 key = "static-nodes.json" 252 } 253 254 if node.Spec.Genesis != nil { 255 configmap.Data["genesis.json"] = genesis 256 if node.Spec.Client == ethereumv1alpha1.GethClient { 257 configmap.Data["geth-init-genesis.sh"] = GethInitGenesisScript 258 } 259 } 260 261 if node.Spec.Import != nil { 262 configmap.Data["import-account.sh"] = importAccountScript 263 } 264 265 if node.Spec.Client == ethereumv1alpha1.NethermindClient { 266 configmap.Data["nethermind_convert_enode_privatekey.sh"] = nethermindConvertEnodePrivateKeyScript 267 configmap.Data["nethermind_copy_keystore.sh"] = nethermindConvertCopyKeystoreScript 268 } 269 270 currentStaticNodes := configmap.Data[key] 271 // update static nodes config if it's empty 272 // update static nodes config if more static nodes has been created 273 if currentStaticNodes == "" || len(currentStaticNodes) < len(staticNodes) { 274 configmap.Data[key] = staticNodes 275 } 276 277 // create empty config for ptivate networks so it won't be ovverriden by 278 if node.Spec.Client == ethereumv1alpha1.NethermindClient && node.Spec.Genesis != nil { 279 configmap.Data["empty.cfg"] = "{}" 280 } 281 282 } 283 284 // reconcileConfigmap creates genesis config map if it doesn't exist or update it 285 func (r *NodeReconciler) reconcileConfigmap(ctx context.Context, node *ethereumv1alpha1.Node) error { 286 287 var genesis string 288 289 log := log.FromContext(ctx) 290 291 configmap := &corev1.ConfigMap{ 292 ObjectMeta: metav1.ObjectMeta{ 293 Name: node.Name, 294 Namespace: node.Namespace, 295 }, 296 } 297 298 client, err := ethereumClients.NewClient(node) 299 if err != nil { 300 return err 301 } 302 303 staticNodes := client.EncodeStaticNodes() 304 305 // private network with custom genesis 306 if node.Spec.Genesis != nil { 307 308 // create client specific genesis configuration 309 if genesis, err = client.Genesis(); err != nil { 310 return err 311 } 312 } 313 314 _, err = ctrl.CreateOrUpdate(ctx, r.Client, configmap, func() error { 315 if err := ctrl.SetControllerReference(node, configmap, r.Scheme); err != nil { 316 log.Error(err, "Unable to set controller reference on genesis configmap") 317 return err 318 } 319 320 r.specConfigmap(node, configmap, genesis, staticNodes) 321 322 return nil 323 }) 324 325 return err 326 } 327 328 // specPVC update node data pvc spec 329 func (r *NodeReconciler) specPVC(node *ethereumv1alpha1.Node, pvc *corev1.PersistentVolumeClaim) { 330 request := corev1.ResourceList{ 331 corev1.ResourceStorage: resource.MustParse(node.Spec.Resources.Storage), 332 } 333 334 // spec is immutable after creation except resources.requests for bound claims 335 if !pvc.CreationTimestamp.IsZero() { 336 pvc.Spec.Resources.Requests = request 337 return 338 } 339 340 pvc.ObjectMeta.Labels = node.GetLabels() 341 pvc.Spec = corev1.PersistentVolumeClaimSpec{ 342 AccessModes: []corev1.PersistentVolumeAccessMode{ 343 corev1.ReadWriteOnce, 344 }, 345 Resources: corev1.VolumeResourceRequirements{ 346 Requests: request, 347 }, 348 StorageClassName: node.Spec.Resources.StorageClass, 349 } 350 } 351 352 // reconcilePVC creates node data pvc if it doesn't exist 353 func (r *NodeReconciler) reconcilePVC(ctx context.Context, node *ethereumv1alpha1.Node) error { 354 355 pvc := &corev1.PersistentVolumeClaim{ 356 ObjectMeta: metav1.ObjectMeta{ 357 Name: node.Name, 358 Namespace: node.Namespace, 359 }, 360 } 361 362 _, err := ctrl.CreateOrUpdate(ctx, r.Client, pvc, func() error { 363 if err := ctrl.SetControllerReference(node, pvc, r.Scheme); err != nil { 364 return err 365 } 366 r.specPVC(node, pvc) 367 return nil 368 }) 369 370 return err 371 } 372 373 // createNodeVolumes creates all the required volumes for the node 374 func (r *NodeReconciler) createNodeVolumes(node *ethereumv1alpha1.Node) []corev1.Volume { 375 376 volumes := []corev1.Volume{} 377 projections := []corev1.VolumeProjection{} 378 379 // authenticated APIs jwt secret 380 if node.Spec.JWTSecretName != "" { 381 jwtSecretProjection := corev1.VolumeProjection{ 382 Secret: &corev1.SecretProjection{ 383 LocalObjectReference: corev1.LocalObjectReference{ 384 Name: node.Spec.JWTSecretName, 385 }, 386 Items: []corev1.KeyToPath{ 387 { 388 Key: "secret", 389 Path: "jwt.secret", 390 }, 391 }, 392 }, 393 } 394 projections = append(projections, jwtSecretProjection) 395 } 396 397 // nodekey (node private key) projection 398 if node.Spec.NodePrivateKeySecretName != "" { 399 nodekeyProjection := corev1.VolumeProjection{ 400 Secret: &corev1.SecretProjection{ 401 LocalObjectReference: corev1.LocalObjectReference{ 402 Name: node.Spec.NodePrivateKeySecretName, 403 }, 404 Items: []corev1.KeyToPath{ 405 { 406 Key: "key", 407 Path: "nodekey", 408 }, 409 }, 410 }, 411 } 412 projections = append(projections, nodekeyProjection) 413 } 414 415 // importing ethereum account 416 if node.Spec.Import != nil { 417 // account private key projection 418 privateKeyProjection := corev1.VolumeProjection{ 419 Secret: &corev1.SecretProjection{ 420 LocalObjectReference: corev1.LocalObjectReference{ 421 Name: node.Spec.Import.PrivateKeySecretName, 422 }, 423 Items: []corev1.KeyToPath{ 424 { 425 Key: "key", 426 Path: "account.key", 427 }, 428 }, 429 }, 430 } 431 projections = append(projections, privateKeyProjection) 432 433 // account password projection 434 passwordProjection := corev1.VolumeProjection{ 435 Secret: &corev1.SecretProjection{ 436 LocalObjectReference: corev1.LocalObjectReference{ 437 Name: node.Spec.Import.PasswordSecretName, 438 }, 439 Items: []corev1.KeyToPath{ 440 { 441 Key: "password", 442 Path: "account.password", 443 }, 444 }, 445 }, 446 } 447 projections = append(projections, passwordProjection) 448 449 // nethermind : account keystore 450 if node.Spec.Client == ethereumv1alpha1.NethermindClient { 451 accountKeystoreProjection := corev1.VolumeProjection{ 452 Secret: &corev1.SecretProjection{ 453 LocalObjectReference: corev1.LocalObjectReference{ 454 Name: node.Name, 455 }, 456 }, 457 } 458 projections = append(projections, accountKeystoreProjection) 459 } 460 } 461 462 if len(projections) != 0 { 463 secretsVolume := corev1.Volume{ 464 Name: "secrets", 465 VolumeSource: corev1.VolumeSource{ 466 Projected: &corev1.ProjectedVolumeSource{ 467 Sources: projections, 468 }, 469 }, 470 } 471 volumes = append(volumes, secretsVolume) 472 } 473 474 configVolume := corev1.Volume{ 475 Name: "config", 476 VolumeSource: corev1.VolumeSource{ 477 ConfigMap: &corev1.ConfigMapVolumeSource{ 478 LocalObjectReference: corev1.LocalObjectReference{ 479 Name: node.Name, 480 }, 481 }, 482 }, 483 } 484 volumes = append(volumes, configVolume) 485 486 dataVolume := corev1.Volume{ 487 Name: "data", 488 VolumeSource: corev1.VolumeSource{ 489 PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ 490 ClaimName: node.Name, 491 }, 492 }, 493 } 494 volumes = append(volumes, dataVolume) 495 496 return volumes 497 } 498 499 // createNodeVolumeMounts creates all required volume mounts for the node 500 func (r *NodeReconciler) createNodeVolumeMounts(node *ethereumv1alpha1.Node, homedir string) []corev1.VolumeMount { 501 502 volumeMounts := []corev1.VolumeMount{} 503 504 if node.Spec.NodePrivateKeySecretName != "" || node.Spec.Import != nil || node.Spec.JWTSecretName != "" { 505 secretsMount := corev1.VolumeMount{ 506 Name: "secrets", 507 MountPath: shared.PathSecrets(homedir), 508 ReadOnly: true, 509 } 510 volumeMounts = append(volumeMounts, secretsMount) 511 } 512 513 configMount := corev1.VolumeMount{ 514 Name: "config", 515 MountPath: shared.PathConfig(homedir), 516 ReadOnly: true, 517 } 518 volumeMounts = append(volumeMounts, configMount) 519 520 dataMount := corev1.VolumeMount{ 521 Name: "data", 522 MountPath: shared.PathData(homedir), 523 } 524 volumeMounts = append(volumeMounts, dataMount) 525 526 return volumeMounts 527 } 528 529 // specStatefulset updates node statefulset spec 530 func (r *NodeReconciler) specStatefulset(node *ethereumv1alpha1.Node, sts *appsv1.StatefulSet, homedir string, args []string, volumes []corev1.Volume, volumeMounts []corev1.VolumeMount) { 531 labels := node.GetLabels() 532 // used by geth to init genesis and import account(s) 533 initContainers := []corev1.Container{} 534 535 client := node.Spec.Client 536 ports := []corev1.ContainerPort{ 537 { 538 Name: "discovery", 539 ContainerPort: int32(node.Spec.P2PPort), 540 Protocol: corev1.ProtocolUDP, 541 }, 542 { 543 Name: "p2p", 544 ContainerPort: int32(node.Spec.P2PPort), 545 }, 546 } 547 548 if node.Spec.RPC { 549 ports = append(ports, corev1.ContainerPort{ 550 Name: "rpc", 551 ContainerPort: int32(node.Spec.RPCPort), 552 }) 553 } 554 555 if node.Spec.WS { 556 ports = append(ports, corev1.ContainerPort{ 557 Name: "ws", 558 ContainerPort: int32(node.Spec.WSPort), 559 }) 560 } 561 562 if node.Spec.Engine { 563 ports = append(ports, corev1.ContainerPort{ 564 Name: "engine", 565 ContainerPort: int32(node.Spec.EnginePort), 566 }) 567 } 568 569 if node.Spec.GraphQL { 570 targetPort := node.Spec.GraphQLPort 571 if client == ethereumv1alpha1.GethClient { 572 targetPort = node.Spec.RPCPort 573 } 574 ports = append(ports, corev1.ContainerPort{ 575 Name: "graphql", 576 ContainerPort: int32(targetPort), 577 }) 578 } 579 580 // node client container 581 nodeContainer := corev1.Container{ 582 Name: "node", 583 Image: node.Spec.Image, 584 Args: args, 585 Ports: ports, 586 Resources: corev1.ResourceRequirements{ 587 Requests: corev1.ResourceList{ 588 corev1.ResourceCPU: resource.MustParse(node.Spec.Resources.CPU), 589 corev1.ResourceMemory: resource.MustParse(node.Spec.Resources.Memory), 590 }, 591 Limits: corev1.ResourceList{ 592 corev1.ResourceCPU: resource.MustParse(node.Spec.Resources.CPULimit), 593 corev1.ResourceMemory: resource.MustParse(node.Spec.Resources.MemoryLimit), 594 }, 595 }, 596 VolumeMounts: volumeMounts, 597 } 598 599 if node.Spec.Client == ethereumv1alpha1.GethClient { 600 if node.Spec.Genesis != nil { 601 initGenesis := corev1.Container{ 602 Name: "init-geth-genesis", 603 Image: node.Spec.Image, 604 Env: []corev1.EnvVar{ 605 { 606 Name: shared.EnvDataPath, 607 Value: shared.PathData(homedir), 608 }, 609 { 610 Name: shared.EnvConfigPath, 611 Value: shared.PathConfig(homedir), 612 }, 613 }, 614 Command: []string{"/bin/sh"}, 615 Args: []string{fmt.Sprintf("%s/geth-init-genesis.sh", shared.PathConfig(homedir))}, 616 VolumeMounts: volumeMounts, 617 } 618 initContainers = append(initContainers, initGenesis) 619 } 620 if node.Spec.Import != nil { 621 importAccount := corev1.Container{ 622 Name: "import-account", 623 Image: node.Spec.Image, 624 Env: []corev1.EnvVar{ 625 { 626 Name: shared.EnvDataPath, 627 Value: shared.PathData(homedir), 628 }, 629 { 630 Name: shared.EnvSecretsPath, 631 Value: shared.PathSecrets(homedir), 632 }, 633 }, 634 Command: []string{"/bin/sh"}, 635 Args: []string{fmt.Sprintf("%s/import-account.sh", shared.PathConfig(homedir))}, 636 VolumeMounts: volumeMounts, 637 } 638 initContainers = append(initContainers, importAccount) 639 } 640 641 } else if node.Spec.Client == ethereumv1alpha1.NethermindClient { 642 if node.Spec.NodePrivateKeySecretName != "" { 643 convertEnodePrivateKey := corev1.Container{ 644 Name: "convert-enode-privatekey", 645 Image: shared.BusyboxImage, 646 Env: []corev1.EnvVar{ 647 { 648 Name: shared.EnvDataPath, 649 Value: shared.PathData(homedir), 650 }, 651 { 652 Name: shared.EnvSecretsPath, 653 Value: shared.PathSecrets(homedir), 654 }, 655 }, 656 Command: []string{"/bin/sh"}, 657 Args: []string{fmt.Sprintf("%s/nethermind_convert_enode_privatekey.sh", shared.PathConfig(homedir))}, 658 VolumeMounts: volumeMounts, 659 } 660 initContainers = append(initContainers, convertEnodePrivateKey) 661 } 662 663 if node.Spec.Import != nil { 664 copyKeystore := corev1.Container{ 665 Name: "copy-keystore", 666 Image: shared.BusyboxImage, 667 Env: []corev1.EnvVar{ 668 { 669 Name: shared.EnvDataPath, 670 Value: shared.PathData(homedir), 671 }, 672 { 673 Name: shared.EnvSecretsPath, 674 Value: shared.PathSecrets(homedir), 675 }, 676 { 677 Name: envCoinbase, 678 Value: strings.ToLower(string(node.Spec.Coinbase))[2:], 679 }, 680 }, 681 Command: []string{"/bin/sh"}, 682 Args: []string{fmt.Sprintf("%s/nethermind_copy_keystore.sh", shared.PathConfig(homedir))}, 683 VolumeMounts: volumeMounts, 684 } 685 initContainers = append(initContainers, copyKeystore) 686 } 687 } 688 689 sts.ObjectMeta.Labels = labels 690 if sts.Spec.Selector == nil { 691 sts.Spec.Selector = &metav1.LabelSelector{} 692 } 693 694 replicas := int32(*node.Spec.Replicas) 695 696 sts.Spec.Replicas = &replicas 697 sts.Spec.ServiceName = node.Name 698 sts.Spec.Selector.MatchLabels = labels 699 sts.Spec.Template.ObjectMeta.Labels = labels 700 sts.Spec.Template.Spec = corev1.PodSpec{ 701 SecurityContext: shared.SecurityContext(), 702 Volumes: volumes, 703 InitContainers: initContainers, 704 Containers: []corev1.Container{nodeContainer}, 705 } 706 } 707 708 // reconcileStatefulSet creates node statefulset if it doesn't exist, update it if it does exist 709 func (r *NodeReconciler) reconcileStatefulSet(ctx context.Context, node *ethereumv1alpha1.Node) error { 710 711 sts := &appsv1.StatefulSet{ 712 ObjectMeta: metav1.ObjectMeta{ 713 Name: node.Name, 714 Namespace: node.Namespace, 715 }, 716 } 717 718 client, err := ethereumClients.NewClient(node) 719 if err != nil { 720 return err 721 } 722 homedir := client.HomeDir() 723 args := client.Args() 724 args = append(args, node.Spec.ExtraArgs.Encode(false)...) 725 volumes := r.createNodeVolumes(node) 726 mounts := r.createNodeVolumeMounts(node, homedir) 727 728 _, err = ctrl.CreateOrUpdate(ctx, r.Client, sts, func() error { 729 if err := ctrl.SetControllerReference(node, sts, r.Scheme); err != nil { 730 return err 731 } 732 r.specStatefulset(node, sts, homedir, args, volumes, mounts) 733 return nil 734 }) 735 736 return err 737 } 738 739 // specSecret creates keystore from account private key for nethermind client 740 func (r *NodeReconciler) specSecret(ctx context.Context, node *ethereumv1alpha1.Node, secret *corev1.Secret) error { 741 secret.ObjectMeta.Labels = node.GetLabels() 742 743 if node.Spec.Import != nil && node.Spec.Client == ethereumv1alpha1.NethermindClient { 744 key := types.NamespacedName{ 745 Name: node.Spec.Import.PrivateKeySecretName, 746 Namespace: node.Namespace, 747 } 748 749 privateKey, err := shared.GetSecret(ctx, r.Client, key, "key") 750 if err != nil { 751 return err 752 } 753 754 key = types.NamespacedName{ 755 Name: node.Spec.Import.PasswordSecretName, 756 Namespace: node.Namespace, 757 } 758 759 password, err := shared.GetSecret(ctx, r.Client, key, "password") 760 if err != nil { 761 return err 762 } 763 764 account, err := KeyStoreFromPrivateKey(privateKey, password) 765 if err != nil { 766 return err 767 } 768 769 secret.Data = map[string][]byte{ 770 "account": account, 771 } 772 } 773 774 return nil 775 } 776 777 // reconcileSecret creates node secret if it doesn't exist, update it if it exists 778 func (r *NodeReconciler) reconcileSecret(ctx context.Context, node *ethereumv1alpha1.Node) (publicKey string, err error) { 779 780 secret := &corev1.Secret{ 781 ObjectMeta: metav1.ObjectMeta{ 782 Name: node.Name, 783 Namespace: node.Namespace, 784 }, 785 } 786 787 // pubkey is required by the caller 788 // 1. read the private key secret content 789 // 2. derive public key from the private key 790 if node.Spec.NodePrivateKeySecretName != "" { 791 key := types.NamespacedName{ 792 Name: node.Spec.NodePrivateKeySecretName, 793 Namespace: node.Namespace, 794 } 795 796 var nodekey string 797 nodekey, err = shared.GetSecret(ctx, r.Client, key, "key") 798 if err != nil { 799 return 800 } 801 802 // hex private key without the leading 0x 803 publicKey, err = helpers.DerivePublicKey(nodekey) 804 if err != nil { 805 return 806 } 807 } 808 809 _, err = ctrl.CreateOrUpdate(ctx, r.Client, secret, func() error { 810 if err := ctrl.SetControllerReference(node, secret, r.Scheme); err != nil { 811 return err 812 } 813 814 return r.specSecret(ctx, node, secret) 815 }) 816 817 return 818 } 819 820 // specService updates node service spec 821 func (r *NodeReconciler) specService(node *ethereumv1alpha1.Node, svc *corev1.Service) { 822 labels := node.GetLabels() 823 824 svc.ObjectMeta.Labels = labels 825 svc.Spec.Ports = []corev1.ServicePort{ 826 { 827 Name: "discovery", 828 Port: int32(node.Spec.P2PPort), 829 TargetPort: intstr.FromString("discovery"), 830 Protocol: corev1.ProtocolUDP, 831 }, 832 { 833 Name: "p2p", 834 Port: int32(node.Spec.P2PPort), 835 TargetPort: intstr.FromString("p2p"), 836 }, 837 } 838 839 if node.Spec.RPC { 840 svc.Spec.Ports = append(svc.Spec.Ports, corev1.ServicePort{ 841 Name: "rpc", 842 Port: int32(node.Spec.RPCPort), 843 TargetPort: intstr.FromString("rpc"), 844 }) 845 } 846 847 if node.Spec.WS { 848 svc.Spec.Ports = append(svc.Spec.Ports, corev1.ServicePort{ 849 Name: "ws", 850 Port: int32(node.Spec.WSPort), 851 TargetPort: intstr.FromString("ws"), 852 }) 853 } 854 855 if node.Spec.Engine { 856 svc.Spec.Ports = append(svc.Spec.Ports, corev1.ServicePort{ 857 Name: "engine", 858 Port: int32(node.Spec.EnginePort), 859 TargetPort: intstr.FromString("engine"), 860 }) 861 } 862 863 if node.Spec.GraphQL { 864 svc.Spec.Ports = append(svc.Spec.Ports, corev1.ServicePort{ 865 Name: "graphql", 866 Port: int32(node.Spec.GraphQLPort), 867 TargetPort: intstr.FromString("graphql"), 868 }) 869 } 870 871 svc.Spec.Selector = labels 872 } 873 874 // reconcileService reconciles node service 875 func (r *NodeReconciler) reconcileService(ctx context.Context, node *ethereumv1alpha1.Node) (ip string, err error) { 876 877 svc := &corev1.Service{ 878 ObjectMeta: metav1.ObjectMeta{ 879 Name: node.Name, 880 Namespace: node.Namespace, 881 }, 882 } 883 884 _, err = ctrl.CreateOrUpdate(ctx, r.Client, svc, func() error { 885 if err = ctrl.SetControllerReference(node, svc, r.Scheme); err != nil { 886 return err 887 } 888 889 r.specService(node, svc) 890 891 return nil 892 }) 893 894 if err != nil { 895 return 896 } 897 898 ip = svc.Spec.ClusterIP 899 900 return 901 } 902 903 // SetupWithManager adds reconciler to the manager 904 func (r *NodeReconciler) SetupWithManager(mgr ctrl.Manager) error { 905 pred := predicate.GenerationChangedPredicate{} 906 return ctrl.NewControllerManagedBy(mgr). 907 For(ðereumv1alpha1.Node{}). 908 WithEventFilter(pred). 909 Owns(&appsv1.StatefulSet{}). 910 Owns(&corev1.Service{}). 911 Owns(&corev1.Secret{}). 912 Owns(&corev1.PersistentVolumeClaim{}). 913 Owns(&corev1.ConfigMap{}). 914 Complete(r) 915 }