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 := &ethereumv1alpha1.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(&ethereumv1alpha1.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  }