github.com/kotalco/kotal@v0.3.0/controllers/near/node_controller.go (about) 1 package controllers 2 3 import ( 4 "context" 5 _ "embed" 6 "fmt" 7 8 nearv1alpha1 "github.com/kotalco/kotal/apis/near/v1alpha1" 9 nearClients "github.com/kotalco/kotal/clients/near" 10 "github.com/kotalco/kotal/controllers/shared" 11 appsv1 "k8s.io/api/apps/v1" 12 corev1 "k8s.io/api/core/v1" 13 "k8s.io/apimachinery/pkg/api/resource" 14 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 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 22 // NodeReconciler reconciles a Node object 23 type NodeReconciler struct { 24 shared.Reconciler 25 } 26 27 const ( 28 envNetwork = "KOTAL_NEAR_NETWORK" 29 ) 30 31 var ( 32 //go:embed init_near_node.sh 33 InitNearNode string 34 //go:embed copy_node_key.sh 35 CopyNodeKey string 36 //go:embed copy_validator_key.sh 37 CopyValidatorKey string 38 ) 39 40 // +kubebuilder:rbac:groups=near.kotal.io,resources=nodes,verbs=get;list;watch;create;update;patch;delete 41 // +kubebuilder:rbac:groups=near.kotal.io,resources=nodes/status,verbs=get;update;patch 42 // +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=watch;get;list;create;update;delete 43 // +kubebuilder:rbac:groups=core,resources=configmaps;persistentvolumeclaims;services,verbs=watch;get;create;update;list;delete 44 45 func (r *NodeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { 46 defer shared.IgnoreConflicts(&err) 47 48 var node nearv1alpha1.Node 49 50 if err = r.Client.Get(ctx, req.NamespacedName, &node); err != nil { 51 err = client.IgnoreNotFound(err) 52 return 53 } 54 55 // default the node if webhooks are disabled 56 if !shared.IsWebhookEnabled() { 57 node.Default() 58 } 59 60 shared.UpdateLabels(&node, "nearcore", node.Spec.Network) 61 62 // reconcile persistent volume claim 63 if err = r.ReconcileOwned(ctx, &node, &corev1.PersistentVolumeClaim{}, func(obj client.Object) error { 64 r.specPVC(&node, obj.(*corev1.PersistentVolumeClaim)) 65 return nil 66 }); err != nil { 67 return 68 } 69 70 // reconcile config map 71 if err = r.ReconcileOwned(ctx, &node, &corev1.ConfigMap{}, func(obj client.Object) error { 72 r.specConfigmap(&node, obj.(*corev1.ConfigMap)) 73 return nil 74 }); err != nil { 75 return 76 } 77 78 // reconcile service 79 if err = r.ReconcileOwned(ctx, &node, &corev1.Service{}, func(obj client.Object) error { 80 r.specService(&node, obj.(*corev1.Service)) 81 return nil 82 }); err != nil { 83 return 84 } 85 86 // reconcile stateful set 87 if err = r.ReconcileOwned(ctx, &node, &appsv1.StatefulSet{}, func(obj client.Object) error { 88 client := nearClients.NewClient(&node) 89 homeDir := client.HomeDir() 90 args := client.Args() 91 args = append(args, node.Spec.ExtraArgs.Encode(false)...) 92 93 r.specStatefulSet(&node, obj.(*appsv1.StatefulSet), homeDir, args) 94 return nil 95 }); err != nil { 96 return 97 } 98 99 if err = r.updateStatus(ctx, &node); err != nil { 100 return 101 } 102 103 return 104 } 105 106 // updateStatus updates NEAR node status 107 func (r *NodeReconciler) updateStatus(ctx context.Context, peer *nearv1alpha1.Node) error { 108 peer.Status.Client = "nearcore" 109 110 if err := r.Status().Update(ctx, peer); err != nil { 111 log.FromContext(ctx).Error(err, "unable to update node status") 112 return err 113 } 114 115 return nil 116 } 117 118 // specService updates NEAR node service spec 119 func (r *NodeReconciler) specService(node *nearv1alpha1.Node, svc *corev1.Service) { 120 labels := node.Labels 121 122 svc.ObjectMeta.Labels = labels 123 124 svc.Spec.Ports = []corev1.ServicePort{ 125 { 126 Name: "p2p", 127 Port: int32(node.Spec.P2PPort), 128 TargetPort: intstr.FromString("p2p"), 129 }, 130 { 131 Name: "discovery", 132 Port: int32(node.Spec.P2PPort), 133 TargetPort: intstr.FromString("discovery"), 134 Protocol: corev1.ProtocolUDP, 135 }, 136 } 137 138 if node.Spec.RPC { 139 svc.Spec.Ports = append(svc.Spec.Ports, corev1.ServicePort{ 140 Name: "rpc", 141 Port: int32(node.Spec.RPCPort), 142 TargetPort: intstr.FromString("rpc"), 143 }) 144 svc.Spec.Ports = append(svc.Spec.Ports, corev1.ServicePort{ 145 Name: "prometheus", 146 Port: int32(node.Spec.PrometheusPort), 147 TargetPort: intstr.FromString("prometheus"), 148 }) 149 } 150 151 svc.Spec.Selector = labels 152 } 153 154 // specPVC updates NEAR node persistent volume claim 155 func (n *NodeReconciler) specPVC(peer *nearv1alpha1.Node, pvc *corev1.PersistentVolumeClaim) { 156 request := corev1.ResourceList{ 157 corev1.ResourceStorage: resource.MustParse(peer.Spec.Resources.Storage), 158 } 159 160 // spec is immutable after creation except resources.requests for bound claims 161 if !pvc.CreationTimestamp.IsZero() { 162 pvc.Spec.Resources.Requests = request 163 return 164 } 165 166 pvc.ObjectMeta.Labels = peer.Labels 167 pvc.Spec = corev1.PersistentVolumeClaimSpec{ 168 AccessModes: []corev1.PersistentVolumeAccessMode{ 169 corev1.ReadWriteOnce, 170 }, 171 Resources: corev1.VolumeResourceRequirements{ 172 Requests: request, 173 }, 174 StorageClassName: peer.Spec.Resources.StorageClass, 175 } 176 } 177 178 // specConfigmap updates node configmap 179 func (n *NodeReconciler) specConfigmap(node *nearv1alpha1.Node, configmap *corev1.ConfigMap) { 180 configmap.ObjectMeta.Labels = node.Labels 181 182 if configmap.Data == nil { 183 configmap.Data = map[string]string{} 184 } 185 186 configmap.Data["init_near_node.sh"] = InitNearNode 187 configmap.Data["copy_node_key.sh"] = CopyNodeKey 188 configmap.Data["copy_validator_key.sh"] = CopyValidatorKey 189 190 } 191 192 func (r *NodeReconciler) createVolumes(node *nearv1alpha1.Node) []corev1.Volume { 193 194 var volumeProjections []corev1.VolumeProjection 195 196 volumes := []corev1.Volume{ 197 { 198 Name: "data", 199 VolumeSource: corev1.VolumeSource{ 200 PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ 201 ClaimName: node.Name, 202 }, 203 }, 204 }, 205 { 206 Name: "config", 207 VolumeSource: corev1.VolumeSource{ 208 ConfigMap: &corev1.ConfigMapVolumeSource{ 209 LocalObjectReference: corev1.LocalObjectReference{ 210 Name: node.Name, 211 }, 212 }, 213 }, 214 }, 215 } 216 217 if node.Spec.NodePrivateKeySecretName != "" { 218 volumeProjections = append(volumeProjections, corev1.VolumeProjection{ 219 Secret: &corev1.SecretProjection{ 220 LocalObjectReference: corev1.LocalObjectReference{ 221 Name: node.Spec.NodePrivateKeySecretName, 222 }, 223 Items: []corev1.KeyToPath{ 224 { 225 Key: "key", 226 Path: "node_key.json", 227 }, 228 }, 229 }, 230 }) 231 } 232 233 if node.Spec.ValidatorSecretName != "" { 234 volumeProjections = append(volumeProjections, corev1.VolumeProjection{ 235 Secret: &corev1.SecretProjection{ 236 LocalObjectReference: corev1.LocalObjectReference{ 237 Name: node.Spec.ValidatorSecretName, 238 }, 239 Items: []corev1.KeyToPath{ 240 { 241 Key: "key", 242 Path: "validator_key.json", 243 }, 244 }, 245 }, 246 }) 247 } 248 249 secretsVolume := corev1.Volume{ 250 Name: "secrets", 251 VolumeSource: corev1.VolumeSource{ 252 Projected: &corev1.ProjectedVolumeSource{ 253 Sources: volumeProjections, 254 }, 255 }, 256 } 257 volumes = append(volumes, secretsVolume) 258 259 return volumes 260 } 261 262 func (r *NodeReconciler) createVolumeMounts(node *nearv1alpha1.Node, homeDir string) []corev1.VolumeMount { 263 mounts := []corev1.VolumeMount{ 264 { 265 Name: "data", 266 MountPath: shared.PathData(homeDir), 267 }, 268 { 269 Name: "config", 270 MountPath: shared.PathConfig(homeDir), 271 }, 272 } 273 274 if node.Spec.NodePrivateKeySecretName != "" || node.Spec.ValidatorSecretName != "" { 275 mounts = append(mounts, corev1.VolumeMount{ 276 Name: "secrets", 277 MountPath: shared.PathSecrets(homeDir), 278 }) 279 } 280 281 return mounts 282 } 283 284 // specStatefulSet updates node statefulset spec 285 func (r *NodeReconciler) specStatefulSet(node *nearv1alpha1.Node, sts *appsv1.StatefulSet, homeDir string, args []string) { 286 287 sts.ObjectMeta.Labels = node.Labels 288 289 initContainers := []corev1.Container{ 290 { 291 Name: "init-near-node", 292 Image: node.Spec.Image, 293 Env: []corev1.EnvVar{ 294 { 295 Name: shared.EnvDataPath, 296 Value: shared.PathData(homeDir), 297 }, 298 { 299 Name: envNetwork, 300 Value: node.Spec.Network, 301 }, 302 }, 303 Command: []string{"/bin/sh"}, 304 Args: []string{fmt.Sprintf("%s/init_near_node.sh", shared.PathConfig(homeDir))}, 305 VolumeMounts: r.createVolumeMounts(node, homeDir), 306 }, 307 } 308 309 if node.Spec.NodePrivateKeySecretName != "" { 310 initContainers = append(initContainers, corev1.Container{ 311 Name: "copy-node-key", 312 Image: shared.BusyboxImage, 313 Command: []string{"/bin/sh"}, 314 Env: []corev1.EnvVar{ 315 { 316 Name: shared.EnvDataPath, 317 Value: shared.PathData(homeDir), 318 }, 319 { 320 Name: shared.EnvSecretsPath, 321 Value: shared.PathSecrets(homeDir), 322 }, 323 }, 324 Args: []string{fmt.Sprintf("%s/copy_node_key.sh", shared.PathConfig(homeDir))}, 325 VolumeMounts: r.createVolumeMounts(node, homeDir), 326 }) 327 } 328 329 if node.Spec.ValidatorSecretName != "" { 330 initContainers = append(initContainers, corev1.Container{ 331 Name: "copy-validator-key", 332 Image: shared.BusyboxImage, 333 Command: []string{"/bin/sh"}, 334 Env: []corev1.EnvVar{ 335 { 336 Name: shared.EnvDataPath, 337 Value: shared.PathData(homeDir), 338 }, 339 { 340 Name: shared.EnvSecretsPath, 341 Value: shared.PathSecrets(homeDir), 342 }, 343 }, 344 Args: []string{fmt.Sprintf("%s/copy_validator_key.sh", shared.PathConfig(homeDir))}, 345 VolumeMounts: r.createVolumeMounts(node, homeDir), 346 }) 347 } 348 349 ports := []corev1.ContainerPort{ 350 { 351 Name: "p2p", 352 ContainerPort: int32(node.Spec.P2PPort), 353 }, 354 { 355 Name: "discovery", 356 ContainerPort: int32(node.Spec.P2PPort), 357 Protocol: corev1.ProtocolUDP, 358 }, 359 } 360 361 if node.Spec.RPC { 362 ports = append(ports, corev1.ContainerPort{ 363 Name: "rpc", 364 ContainerPort: int32(node.Spec.RPCPort), 365 }) 366 ports = append(ports, corev1.ContainerPort{ 367 Name: "prometheus", 368 ContainerPort: int32(node.Spec.PrometheusPort), 369 }) 370 } 371 372 replicas := int32(*node.Spec.Replicas) 373 374 sts.Spec = appsv1.StatefulSetSpec{ 375 Selector: &metav1.LabelSelector{ 376 MatchLabels: node.Labels, 377 }, 378 ServiceName: node.Name, 379 Replicas: &replicas, 380 Template: corev1.PodTemplateSpec{ 381 ObjectMeta: metav1.ObjectMeta{ 382 Labels: node.Labels, 383 }, 384 Spec: corev1.PodSpec{ 385 SecurityContext: shared.SecurityContext(), 386 InitContainers: initContainers, 387 Containers: []corev1.Container{ 388 { 389 Name: "node", 390 Image: node.Spec.Image, 391 Args: args, 392 Ports: ports, 393 VolumeMounts: r.createVolumeMounts(node, homeDir), 394 Resources: corev1.ResourceRequirements{ 395 Requests: corev1.ResourceList{ 396 corev1.ResourceCPU: resource.MustParse(node.Spec.CPU), 397 corev1.ResourceMemory: resource.MustParse(node.Spec.Memory), 398 }, 399 Limits: corev1.ResourceList{ 400 corev1.ResourceCPU: resource.MustParse(node.Spec.CPULimit), 401 corev1.ResourceMemory: resource.MustParse(node.Spec.MemoryLimit), 402 }, 403 }, 404 }, 405 }, 406 Volumes: r.createVolumes(node), 407 }, 408 }, 409 } 410 411 } 412 413 func (r *NodeReconciler) SetupWithManager(mgr ctrl.Manager) error { 414 pred := predicate.GenerationChangedPredicate{} 415 return ctrl.NewControllerManagedBy(mgr). 416 For(&nearv1alpha1.Node{}). 417 WithEventFilter(pred). 418 Owns(&appsv1.StatefulSet{}). 419 Owns(&corev1.ConfigMap{}). 420 Owns(&corev1.PersistentVolumeClaim{}). 421 Owns(&corev1.Service{}). 422 Complete(r) 423 }