github.com/kotalco/kotal@v0.3.0/controllers/polkadot/node_controller.go (about)

     1  package controllers
     2  
     3  import (
     4  	"context"
     5  	_ "embed"
     6  	"fmt"
     7  
     8  	polkadotv1alpha1 "github.com/kotalco/kotal/apis/polkadot/v1alpha1"
     9  	polkadotClients "github.com/kotalco/kotal/clients/polkadot"
    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  )
    19  
    20  // NodeReconciler reconciles a Node object
    21  type NodeReconciler struct {
    22  	shared.Reconciler
    23  }
    24  
    25  var (
    26  	//go:embed convert_node_private_key.sh
    27  	convertNodePrivateKeyScript string
    28  )
    29  
    30  // +kubebuilder:rbac:groups=polkadot.kotal.io,resources=nodes,verbs=get;list;watch;create;update;patch;delete
    31  // +kubebuilder:rbac:groups=polkadot.kotal.io,resources=nodes/status,verbs=get;update;patch
    32  // +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=watch;get;list;create;update;delete
    33  // +kubebuilder:rbac:groups=core,resources=services;configmaps;persistentvolumeclaims,verbs=watch;get;create;update;list;delete
    34  
    35  func (r *NodeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) {
    36  	defer shared.IgnoreConflicts(&err)
    37  
    38  	var node polkadotv1alpha1.Node
    39  
    40  	if err = r.Client.Get(ctx, req.NamespacedName, &node); err != nil {
    41  		err = client.IgnoreNotFound(err)
    42  		return
    43  	}
    44  
    45  	// default the node if webhooks are disabled
    46  	if !shared.IsWebhookEnabled() {
    47  		node.Default()
    48  	}
    49  
    50  	shared.UpdateLabels(&node, "polkadot", node.Spec.Network)
    51  
    52  	// reconcile config map
    53  	if err = r.ReconcileOwned(ctx, &node, &corev1.ConfigMap{}, func(obj client.Object) error {
    54  		r.specConfigmap(&node, obj.(*corev1.ConfigMap))
    55  		return nil
    56  	}); err != nil {
    57  		return
    58  	}
    59  
    60  	// reconcile persistent volume claim
    61  	if err = r.ReconcileOwned(ctx, &node, &corev1.PersistentVolumeClaim{}, func(obj client.Object) error {
    62  		r.specPVC(&node, obj.(*corev1.PersistentVolumeClaim))
    63  		return nil
    64  	}); err != nil {
    65  		return
    66  	}
    67  
    68  	// reconcile service
    69  	if err = r.ReconcileOwned(ctx, &node, &corev1.Service{}, func(obj client.Object) error {
    70  		r.specService(&node, obj.(*corev1.Service))
    71  		return nil
    72  	}); err != nil {
    73  		return
    74  	}
    75  
    76  	// reconcile stateful set
    77  	if err = r.ReconcileOwned(ctx, &node, &appsv1.StatefulSet{}, func(obj client.Object) error {
    78  		client := polkadotClients.NewClient(&node)
    79  		args := client.Args()
    80  		args = append(args, node.Spec.ExtraArgs.Encode(false)...)
    81  		homeDir := client.HomeDir()
    82  
    83  		return r.specStatefulSet(&node, obj.(*appsv1.StatefulSet), homeDir, args)
    84  	}); err != nil {
    85  		return
    86  	}
    87  
    88  	return
    89  }
    90  
    91  // specConfigmap updates polkadot node configmap spec
    92  func (r *NodeReconciler) specConfigmap(node *polkadotv1alpha1.Node, config *corev1.ConfigMap) {
    93  	config.ObjectMeta.Labels = node.Labels
    94  
    95  	if config.Data == nil {
    96  		config.Data = make(map[string]string)
    97  	}
    98  
    99  	config.Data["convert_node_private_key.sh"] = convertNodePrivateKeyScript
   100  }
   101  
   102  // specService updates polkadot node service spec
   103  func (r *NodeReconciler) specService(node *polkadotv1alpha1.Node, svc *corev1.Service) {
   104  	labels := node.Labels
   105  
   106  	svc.ObjectMeta.Labels = labels
   107  
   108  	svc.Spec.Ports = []corev1.ServicePort{
   109  		{
   110  			Name:       "p2p",
   111  			Port:       int32(node.Spec.P2PPort),
   112  			TargetPort: intstr.FromString("p2p"),
   113  		},
   114  	}
   115  
   116  	if node.Spec.Prometheus {
   117  		svc.Spec.Ports = append(svc.Spec.Ports, corev1.ServicePort{
   118  			Name:       "prometheus",
   119  			Port:       int32(node.Spec.PrometheusPort),
   120  			TargetPort: intstr.FromString("prometheus"),
   121  		})
   122  	}
   123  
   124  	if node.Spec.RPC {
   125  		svc.Spec.Ports = append(svc.Spec.Ports, corev1.ServicePort{
   126  			Name:       "rpc",
   127  			Port:       int32(node.Spec.RPCPort),
   128  			TargetPort: intstr.FromString("rpc"),
   129  		})
   130  	}
   131  
   132  	if node.Spec.WS {
   133  		svc.Spec.Ports = append(svc.Spec.Ports, corev1.ServicePort{
   134  			Name:       "ws",
   135  			Port:       int32(node.Spec.WSPort),
   136  			TargetPort: intstr.FromString("ws"),
   137  		})
   138  	}
   139  
   140  	svc.Spec.Selector = labels
   141  }
   142  
   143  // nodeVolumes returns node volumes
   144  func (r *NodeReconciler) nodeVolumes(node *polkadotv1alpha1.Node) (volumes []corev1.Volume) {
   145  	dataVolume := corev1.Volume{
   146  		Name: "data",
   147  		VolumeSource: corev1.VolumeSource{
   148  			PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
   149  				ClaimName: node.Name,
   150  			},
   151  		},
   152  	}
   153  	volumes = append(volumes, dataVolume)
   154  
   155  	configVolume := corev1.Volume{
   156  		Name: "config",
   157  		VolumeSource: corev1.VolumeSource{
   158  			ConfigMap: &corev1.ConfigMapVolumeSource{
   159  				LocalObjectReference: corev1.LocalObjectReference{
   160  					Name: node.Name,
   161  				},
   162  			},
   163  		},
   164  	}
   165  	volumes = append(volumes, configVolume)
   166  
   167  	if node.Spec.NodePrivateKeySecretName != "" {
   168  		secretVolume := corev1.Volume{
   169  			Name: "secret",
   170  			VolumeSource: corev1.VolumeSource{
   171  				Secret: &corev1.SecretVolumeSource{
   172  					SecretName: node.Spec.NodePrivateKeySecretName,
   173  					Items: []corev1.KeyToPath{
   174  						{
   175  							Key:  "key",
   176  							Path: "nodekey",
   177  						},
   178  					},
   179  				},
   180  			},
   181  		}
   182  		volumes = append(volumes, secretVolume)
   183  	}
   184  
   185  	return
   186  }
   187  
   188  // nodeVolumeMounts returns node volume mounts
   189  func (r *NodeReconciler) nodeVolumeMounts(node *polkadotv1alpha1.Node, homeDir string) (mounts []corev1.VolumeMount) {
   190  	dataMount := corev1.VolumeMount{
   191  		Name:      "data",
   192  		MountPath: shared.PathData(homeDir),
   193  	}
   194  	mounts = append(mounts, dataMount)
   195  
   196  	configMount := corev1.VolumeMount{
   197  		Name:      "config",
   198  		MountPath: shared.PathConfig(homeDir),
   199  	}
   200  	mounts = append(mounts, configMount)
   201  
   202  	if node.Spec.NodePrivateKeySecretName != "" {
   203  		secretMount := corev1.VolumeMount{
   204  			Name:      "secret",
   205  			MountPath: shared.PathSecrets(homeDir),
   206  		}
   207  		mounts = append(mounts, secretMount)
   208  	}
   209  	return
   210  }
   211  
   212  // specStatefulSet updates node statefulset spec
   213  func (r *NodeReconciler) specStatefulSet(node *polkadotv1alpha1.Node, sts *appsv1.StatefulSet, homeDir string, args []string) error {
   214  
   215  	sts.ObjectMeta.Labels = node.Labels
   216  
   217  	var initContainers []corev1.Container
   218  
   219  	if node.Spec.NodePrivateKeySecretName != "" {
   220  		convertEnodePrivateKey := corev1.Container{
   221  			Name:  "convert-node-private-key",
   222  			Image: shared.BusyboxImage,
   223  			Env: []corev1.EnvVar{
   224  				{
   225  					Name:  shared.EnvDataPath,
   226  					Value: shared.PathData(homeDir),
   227  				},
   228  				{
   229  					Name:  shared.EnvSecretsPath,
   230  					Value: shared.PathSecrets(homeDir),
   231  				},
   232  			},
   233  			Command:      []string{"/bin/sh"},
   234  			Args:         []string{fmt.Sprintf("%s/convert_node_private_key.sh", shared.PathConfig(homeDir))},
   235  			VolumeMounts: r.nodeVolumeMounts(node, homeDir),
   236  		}
   237  		initContainers = append(initContainers, convertEnodePrivateKey)
   238  	}
   239  
   240  	ports := []corev1.ContainerPort{
   241  		{
   242  			Name:          "p2p",
   243  			ContainerPort: int32(node.Spec.P2PPort),
   244  		},
   245  	}
   246  
   247  	if node.Spec.Prometheus {
   248  		ports = append(ports, corev1.ContainerPort{
   249  			Name:          "prometheus",
   250  			ContainerPort: int32(node.Spec.PrometheusPort),
   251  		})
   252  	}
   253  
   254  	if node.Spec.RPC {
   255  		ports = append(ports, corev1.ContainerPort{
   256  			Name:          "rpc",
   257  			ContainerPort: int32(node.Spec.RPCPort),
   258  		})
   259  	}
   260  
   261  	if node.Spec.WS {
   262  		ports = append(ports, corev1.ContainerPort{
   263  			Name:          "ws",
   264  			ContainerPort: int32(node.Spec.RPCPort),
   265  		})
   266  	}
   267  
   268  	replicas := int32(*node.Spec.Replicas)
   269  
   270  	sts.Spec = appsv1.StatefulSetSpec{
   271  		Selector: &metav1.LabelSelector{
   272  			MatchLabels: node.Labels,
   273  		},
   274  		ServiceName: node.Name,
   275  		Replicas:    &replicas,
   276  		Template: corev1.PodTemplateSpec{
   277  			ObjectMeta: metav1.ObjectMeta{
   278  				Labels: node.Labels,
   279  			},
   280  			Spec: corev1.PodSpec{
   281  				InitContainers:  initContainers,
   282  				SecurityContext: shared.SecurityContext(),
   283  				Containers: []corev1.Container{
   284  					{
   285  						Name:         "node",
   286  						Image:        node.Spec.Image,
   287  						Args:         args,
   288  						Ports:        ports,
   289  						VolumeMounts: r.nodeVolumeMounts(node, homeDir),
   290  						Resources: corev1.ResourceRequirements{
   291  							Requests: map[corev1.ResourceName]resource.Quantity{
   292  								corev1.ResourceCPU:    resource.MustParse(node.Spec.CPU),
   293  								corev1.ResourceMemory: resource.MustParse(node.Spec.Memory),
   294  							},
   295  							Limits: map[corev1.ResourceName]resource.Quantity{
   296  								corev1.ResourceCPU:    resource.MustParse(node.Spec.CPULimit),
   297  								corev1.ResourceMemory: resource.MustParse(node.Spec.MemoryLimit),
   298  							},
   299  						},
   300  					},
   301  				},
   302  				Volumes: r.nodeVolumes(node),
   303  			},
   304  		},
   305  	}
   306  
   307  	return nil
   308  }
   309  
   310  // specPVC updates ipfs peer persistent volume claim
   311  func (r *NodeReconciler) specPVC(node *polkadotv1alpha1.Node, pvc *corev1.PersistentVolumeClaim) {
   312  	request := corev1.ResourceList{
   313  		corev1.ResourceStorage: resource.MustParse(node.Spec.Storage),
   314  	}
   315  
   316  	// spec is immutable after creation except resources.requests for bound claims
   317  	if !pvc.CreationTimestamp.IsZero() {
   318  		pvc.Spec.Resources.Requests = request
   319  		return
   320  	}
   321  
   322  	pvc.ObjectMeta.Labels = node.Labels
   323  	pvc.Spec = corev1.PersistentVolumeClaimSpec{
   324  		AccessModes: []corev1.PersistentVolumeAccessMode{
   325  			corev1.ReadWriteOnce,
   326  		},
   327  		Resources: corev1.VolumeResourceRequirements{
   328  			Requests: request,
   329  		},
   330  		StorageClassName: node.Spec.StorageClass,
   331  	}
   332  }
   333  
   334  func (r *NodeReconciler) SetupWithManager(mgr ctrl.Manager) error {
   335  	return ctrl.NewControllerManagedBy(mgr).
   336  		For(&polkadotv1alpha1.Node{}).
   337  		Owns(&corev1.PersistentVolumeClaim{}).
   338  		Owns(&corev1.Service{}).
   339  		Owns(&corev1.ConfigMap{}).
   340  		Owns(&appsv1.StatefulSet{}).
   341  		Complete(r)
   342  }