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  }