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

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