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

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