github.com/kotalco/kotal@v0.3.0/controllers/ethereum2/beacon_node_controller.go (about)

     1  package controllers
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  
     7  	appsv1 "k8s.io/api/apps/v1"
     8  	corev1 "k8s.io/api/core/v1"
     9  	"k8s.io/apimachinery/pkg/api/resource"
    10  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    11  	"k8s.io/apimachinery/pkg/util/intstr"
    12  	ctrl "sigs.k8s.io/controller-runtime"
    13  	"sigs.k8s.io/controller-runtime/pkg/client"
    14  
    15  	ethereum2v1alpha1 "github.com/kotalco/kotal/apis/ethereum2/v1alpha1"
    16  	ethereum2Clients "github.com/kotalco/kotal/clients/ethereum2"
    17  	"github.com/kotalco/kotal/controllers/shared"
    18  )
    19  
    20  // BeaconNodeReconciler reconciles a Node object
    21  type BeaconNodeReconciler struct {
    22  	shared.Reconciler
    23  }
    24  
    25  // +kubebuilder:rbac:groups=ethereum2.kotal.io,resources=beaconnodes,verbs=get;list;watch;create;update;patch;delete
    26  // +kubebuilder:rbac:groups=ethereum2.kotal.io,resources=beaconnodes/status,verbs=get;update;patch
    27  // +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=watch;get;list;create;update;delete
    28  // +kubebuilder:rbac:groups=core,resources=services;persistentvolumeclaims,verbs=watch;get;create;update;list;delete
    29  
    30  // Reconcile reconciles Ethereum 2.0 beacon node
    31  func (r *BeaconNodeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) {
    32  	defer shared.IgnoreConflicts(&err)
    33  
    34  	var node ethereum2v1alpha1.BeaconNode
    35  
    36  	if err = r.Client.Get(ctx, req.NamespacedName, &node); err != nil {
    37  		err = client.IgnoreNotFound(err)
    38  		return
    39  	}
    40  
    41  	// default the beacon node if webhooks are disabled
    42  	if !shared.IsWebhookEnabled() {
    43  		node.Default()
    44  	}
    45  
    46  	shared.UpdateLabels(&node, string(node.Spec.Client), node.Spec.Network)
    47  
    48  	// reconcile persistent volume clain
    49  	if err = r.ReconcileOwned(ctx, &node, &corev1.PersistentVolumeClaim{}, func(obj client.Object) error {
    50  		r.specPVC(&node, obj.(*corev1.PersistentVolumeClaim))
    51  		return nil
    52  	}); err != nil {
    53  		return
    54  	}
    55  
    56  	// reconcile service
    57  	if err = r.ReconcileOwned(ctx, &node, &corev1.Service{}, func(obj client.Object) error {
    58  		r.specService(&node, obj.(*corev1.Service))
    59  		return nil
    60  	}); err != nil {
    61  		return
    62  	}
    63  
    64  	// reconcile service
    65  	if err = r.ReconcileOwned(ctx, &node, &appsv1.StatefulSet{}, func(obj client.Object) error {
    66  		client, err := ethereum2Clients.NewClient(&node)
    67  		if err != nil {
    68  			return err
    69  		}
    70  
    71  		args := client.Args()
    72  		// encode extra arguments as key=value only if client is numbus
    73  		kv := node.Spec.Client == ethereum2v1alpha1.NimbusClient
    74  		args = append(args, node.Spec.ExtraArgs.Encode(kv)...)
    75  		command := client.Command()
    76  		homeDir := client.HomeDir()
    77  
    78  		r.specStatefulset(&node, obj.(*appsv1.StatefulSet), args, command, homeDir)
    79  		return nil
    80  	}); err != nil {
    81  		return
    82  	}
    83  
    84  	return
    85  }
    86  
    87  func (r *BeaconNodeReconciler) specService(node *ethereum2v1alpha1.BeaconNode, svc *corev1.Service) {
    88  	labels := node.GetLabels()
    89  
    90  	svc.ObjectMeta.Labels = labels
    91  	svc.Spec.Ports = []corev1.ServicePort{
    92  		{
    93  			Name:       "discovery",
    94  			Port:       int32(node.Spec.P2PPort),
    95  			TargetPort: intstr.FromString("discovery"),
    96  			Protocol:   corev1.ProtocolUDP,
    97  		},
    98  		{
    99  			Name:       "p2p",
   100  			Port:       int32(node.Spec.P2PPort),
   101  			TargetPort: intstr.FromString("p2p"),
   102  		},
   103  	}
   104  
   105  	if node.Spec.RPC {
   106  		svc.Spec.Ports = append(svc.Spec.Ports, corev1.ServicePort{
   107  			Name:       "rpc",
   108  			Port:       int32(node.Spec.RPCPort),
   109  			TargetPort: intstr.FromString("rpc"),
   110  		})
   111  	}
   112  
   113  	if node.Spec.GRPC {
   114  		svc.Spec.Ports = append(svc.Spec.Ports, corev1.ServicePort{
   115  			Name:       "grpc",
   116  			Port:       int32(node.Spec.GRPCPort),
   117  			TargetPort: intstr.FromString("grpc"),
   118  		})
   119  	}
   120  
   121  	if node.Spec.REST {
   122  		svc.Spec.Ports = append(svc.Spec.Ports, corev1.ServicePort{
   123  			Name:       "rest",
   124  			Port:       int32(node.Spec.RESTPort),
   125  			TargetPort: intstr.FromString("rest"),
   126  		})
   127  	}
   128  
   129  	svc.Spec.Selector = labels
   130  }
   131  
   132  // specPVC updates beacon node persistent volume claim spec
   133  func (r *BeaconNodeReconciler) specPVC(node *ethereum2v1alpha1.BeaconNode, pvc *corev1.PersistentVolumeClaim) {
   134  
   135  	request := corev1.ResourceList{
   136  		corev1.ResourceStorage: resource.MustParse(node.Spec.Resources.Storage),
   137  	}
   138  
   139  	// spec is immutable after creation except resources.requests for bound claims
   140  	if !pvc.CreationTimestamp.IsZero() {
   141  		pvc.Spec.Resources.Requests = request
   142  		return
   143  	}
   144  
   145  	pvc.Labels = node.GetLabels()
   146  
   147  	pvc.Spec = corev1.PersistentVolumeClaimSpec{
   148  		AccessModes: []corev1.PersistentVolumeAccessMode{
   149  			corev1.ReadWriteOnce,
   150  		},
   151  		Resources: corev1.VolumeResourceRequirements{
   152  			Requests: request,
   153  		},
   154  		StorageClassName: node.Spec.Resources.StorageClass,
   155  	}
   156  }
   157  
   158  // nodeVolumes returns node volumes
   159  func (r *BeaconNodeReconciler) nodeVolumes(node *ethereum2v1alpha1.BeaconNode) (volumes []corev1.Volume) {
   160  	dataVolume := corev1.Volume{
   161  		Name: "data",
   162  		VolumeSource: corev1.VolumeSource{
   163  			PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
   164  				ClaimName: node.Name,
   165  			},
   166  		},
   167  	}
   168  	volumes = append(volumes, dataVolume)
   169  
   170  	// projected volume sources
   171  	volumeProjections := []corev1.VolumeProjection{
   172  		{
   173  			Secret: &corev1.SecretProjection{
   174  				LocalObjectReference: corev1.LocalObjectReference{
   175  					Name: node.Spec.JWTSecretName,
   176  				},
   177  				Items: []corev1.KeyToPath{
   178  					{
   179  						Key:  "secret",
   180  						Path: "jwt.secret",
   181  					},
   182  				},
   183  			},
   184  		},
   185  	}
   186  
   187  	if node.Spec.CertSecretName != "" {
   188  		volumeProjections = append(volumeProjections, corev1.VolumeProjection{
   189  			Secret: &corev1.SecretProjection{
   190  				LocalObjectReference: corev1.LocalObjectReference{
   191  					Name: node.Spec.CertSecretName,
   192  				},
   193  			},
   194  		})
   195  	}
   196  
   197  	volumes = append(volumes, corev1.Volume{
   198  		Name: "secrets",
   199  		VolumeSource: corev1.VolumeSource{
   200  			Projected: &corev1.ProjectedVolumeSource{
   201  				Sources: volumeProjections,
   202  			},
   203  		},
   204  	})
   205  
   206  	return
   207  }
   208  
   209  // nodeVolumeMounts returns node volume mounts
   210  func (r *BeaconNodeReconciler) nodeVolumeMounts(node *ethereum2v1alpha1.BeaconNode, homeDir string) (mounts []corev1.VolumeMount) {
   211  	dataDir := shared.PathData(homeDir)
   212  
   213  	// Nimbus required changing permission of the data dir to be
   214  	// read and write by owner only
   215  	// that's why we mount volume at $HOME
   216  	// but data dir is atatched at $HOME/kota-data
   217  	if node.Spec.Client == ethereum2v1alpha1.NimbusClient {
   218  		dataDir = homeDir
   219  	}
   220  
   221  	dataMount := corev1.VolumeMount{
   222  		Name:      "data",
   223  		MountPath: dataDir,
   224  	}
   225  	mounts = append(mounts, dataMount)
   226  
   227  	secretMount := corev1.VolumeMount{
   228  		Name:      "secrets",
   229  		MountPath: shared.PathSecrets(homeDir),
   230  	}
   231  	mounts = append(mounts, secretMount)
   232  
   233  	return
   234  }
   235  
   236  // specStatefulset updates beacon node statefulset spec
   237  func (r *BeaconNodeReconciler) specStatefulset(node *ethereum2v1alpha1.BeaconNode, sts *appsv1.StatefulSet, args, command []string, homeDir string) {
   238  
   239  	sts.Labels = node.GetLabels()
   240  
   241  	volumes := r.nodeVolumes(node)
   242  	volumeMounts := r.nodeVolumeMounts(node, homeDir)
   243  
   244  	initContainers := []corev1.Container{}
   245  
   246  	if node.Spec.Client == ethereum2v1alpha1.NimbusClient {
   247  		// Nimbus client requires data dir path to be read and write only by the owner 0700
   248  		fixPermissionContainer := corev1.Container{
   249  			Name:  "fix-datadir-permission",
   250  			Image: node.Spec.Image,
   251  			Command: []string{
   252  				"/bin/sh",
   253  				"-c",
   254  			},
   255  			Args: []string{
   256  				fmt.Sprintf(`
   257  					mkdir -p %s &&
   258  					chmod 700 %s`,
   259  					shared.PathData(homeDir),
   260  					shared.PathData(homeDir),
   261  				),
   262  			},
   263  			VolumeMounts: volumeMounts,
   264  		}
   265  		initContainers = append(initContainers, fixPermissionContainer)
   266  
   267  		if node.Spec.CheckpointSyncURL != "" {
   268  			checkpointSyncContainer := corev1.Container{
   269  				Name:    "checkpoint-sync",
   270  				Image:   node.Spec.Image,
   271  				Command: []string{"nimbus_beacon_node", "trustedNodeSync"},
   272  				Args: []string{
   273  					fmt.Sprintf("--network=%s", node.Spec.Network),
   274  					fmt.Sprintf("--data-dir=%s", shared.PathData(homeDir)),
   275  					fmt.Sprintf("--trusted-node-url=%s", node.Spec.CheckpointSyncURL),
   276  				},
   277  				VolumeMounts: volumeMounts,
   278  			}
   279  			initContainers = append(initContainers, checkpointSyncContainer)
   280  		}
   281  	}
   282  
   283  	ports := []corev1.ContainerPort{
   284  		{
   285  			Name:          "discovery",
   286  			ContainerPort: int32(node.Spec.P2PPort),
   287  			Protocol:      corev1.ProtocolUDP,
   288  		},
   289  		{
   290  			Name:          "p2p",
   291  			ContainerPort: int32(node.Spec.P2PPort),
   292  		},
   293  	}
   294  
   295  	if node.Spec.RPC {
   296  		ports = append(ports, corev1.ContainerPort{
   297  			Name:          "rpc",
   298  			ContainerPort: int32(node.Spec.RPCPort),
   299  		})
   300  	}
   301  
   302  	if node.Spec.GRPC {
   303  		ports = append(ports, corev1.ContainerPort{
   304  			Name:          "grpc",
   305  			ContainerPort: int32(node.Spec.GRPCPort),
   306  		})
   307  	}
   308  
   309  	if node.Spec.REST {
   310  		ports = append(ports, corev1.ContainerPort{
   311  			Name:          "rest",
   312  			ContainerPort: int32(node.Spec.RESTPort),
   313  		})
   314  	}
   315  
   316  	replicas := int32(*node.Spec.Replicas)
   317  
   318  	sts.Spec = appsv1.StatefulSetSpec{
   319  		Selector: &metav1.LabelSelector{
   320  			MatchLabels: node.GetLabels(),
   321  		},
   322  		Replicas: &replicas,
   323  		Template: corev1.PodTemplateSpec{
   324  			ObjectMeta: metav1.ObjectMeta{
   325  				Labels: node.GetLabels(),
   326  			},
   327  			Spec: corev1.PodSpec{
   328  				SecurityContext: shared.SecurityContext(),
   329  				InitContainers:  initContainers,
   330  				Containers: []corev1.Container{
   331  					{
   332  						Name:         "node",
   333  						Command:      command,
   334  						Args:         args,
   335  						Ports:        ports,
   336  						Image:        node.Spec.Image,
   337  						VolumeMounts: volumeMounts,
   338  						Resources: corev1.ResourceRequirements{
   339  							Requests: corev1.ResourceList{
   340  								corev1.ResourceCPU:    resource.MustParse(node.Spec.Resources.CPU),
   341  								corev1.ResourceMemory: resource.MustParse(node.Spec.Resources.Memory),
   342  							},
   343  							Limits: corev1.ResourceList{
   344  								corev1.ResourceCPU:    resource.MustParse(node.Spec.Resources.CPULimit),
   345  								corev1.ResourceMemory: resource.MustParse(node.Spec.Resources.MemoryLimit),
   346  							},
   347  						},
   348  					},
   349  				},
   350  				Volumes: volumes,
   351  			},
   352  		},
   353  	}
   354  }
   355  
   356  // SetupWithManager adds reconciler to the manager
   357  func (r *BeaconNodeReconciler) SetupWithManager(mgr ctrl.Manager) error {
   358  	return ctrl.NewControllerManagedBy(mgr).
   359  		For(&ethereum2v1alpha1.BeaconNode{}).
   360  		Owns(&appsv1.StatefulSet{}).
   361  		Owns(&corev1.Service{}).
   362  		Owns(&corev1.PersistentVolumeClaim{}).
   363  		Complete(r)
   364  }