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(ðereum2v1alpha1.BeaconNode{}). 360 Owns(&appsv1.StatefulSet{}). 361 Owns(&corev1.Service{}). 362 Owns(&corev1.PersistentVolumeClaim{}). 363 Complete(r) 364 }