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

     1  package controllers
     2  
     3  import (
     4  	"context"
     5  
     6  	bitcoinClients "github.com/kotalco/kotal/clients/bitcoin"
     7  	appsv1 "k8s.io/api/apps/v1"
     8  	corev1 "k8s.io/api/core/v1"
     9  	"k8s.io/apimachinery/pkg/api/resource"
    10  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    11  	"k8s.io/apimachinery/pkg/util/intstr"
    12  	ctrl "sigs.k8s.io/controller-runtime"
    13  	"sigs.k8s.io/controller-runtime/pkg/client"
    14  	"sigs.k8s.io/controller-runtime/pkg/log"
    15  	"sigs.k8s.io/controller-runtime/pkg/predicate"
    16  
    17  	bitcoinv1alpha1 "github.com/kotalco/kotal/apis/bitcoin/v1alpha1"
    18  	"github.com/kotalco/kotal/controllers/shared"
    19  )
    20  
    21  // NodeReconciler reconciles a Node object
    22  type NodeReconciler struct {
    23  	shared.Reconciler
    24  }
    25  
    26  // +kubebuilder:rbac:groups=bitcoin.kotal.io,resources=nodes,verbs=get;list;watch;create;update;patch;delete
    27  // +kubebuilder:rbac:groups=bitcoin.kotal.io,resources=nodes/status,verbs=get;update;patch
    28  // +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=watch;get;list;create;update;delete
    29  // +kubebuilder:rbac:groups=core,resources=services;persistentvolumeclaims,verbs=watch;get;create;update;list;delete
    30  
    31  // Reconcile Bitcoin node
    32  func (r *NodeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) {
    33  	defer shared.IgnoreConflicts(&err)
    34  
    35  	var node bitcoinv1alpha1.Node
    36  
    37  	if err = r.Client.Get(ctx, req.NamespacedName, &node); err != nil {
    38  		err = client.IgnoreNotFound(err)
    39  		return
    40  	}
    41  
    42  	// default the node if webhooks are disabled
    43  	if !shared.IsWebhookEnabled() {
    44  		node.Default()
    45  	}
    46  
    47  	shared.UpdateLabels(&node, "bitcoind", string(node.Spec.Network))
    48  
    49  	// reconcile persistent volume claim
    50  	if err = r.ReconcileOwned(ctx, &node, &corev1.PersistentVolumeClaim{}, func(obj client.Object) error {
    51  		r.specPVC(&node, obj.(*corev1.PersistentVolumeClaim))
    52  		return nil
    53  	}); err != nil {
    54  		return
    55  	}
    56  
    57  	// reconcile service
    58  	if err = r.ReconcileOwned(ctx, &node, &corev1.Service{}, func(obj client.Object) error {
    59  		r.specService(&node, obj.(*corev1.Service))
    60  		return nil
    61  	}); err != nil {
    62  		return
    63  	}
    64  
    65  	// reconcile statefulset
    66  	if err = r.ReconcileOwned(ctx, &node, &appsv1.StatefulSet{}, func(obj client.Object) error {
    67  		client := bitcoinClients.NewClient(&node, r.Client)
    68  		homeDir := client.HomeDir()
    69  		cmd := client.Command()
    70  		args := client.Args()
    71  		args = append(args, node.Spec.ExtraArgs.Encode(true)...)
    72  		env := client.Env()
    73  		return r.specStatefulSet(&node, obj.(*appsv1.StatefulSet), homeDir, env, cmd, args)
    74  	}); err != nil {
    75  		return
    76  	}
    77  
    78  	if err = r.updateStatus(ctx, &node); err != nil {
    79  		return
    80  	}
    81  
    82  	return
    83  }
    84  
    85  // updateStatus updates Bitcoin node status
    86  func (r *NodeReconciler) updateStatus(ctx context.Context, node *bitcoinv1alpha1.Node) error {
    87  	node.Status.Client = "bitcoincore"
    88  
    89  	if err := r.Status().Update(ctx, node); err != nil {
    90  		log.FromContext(ctx).Error(err, "unable to update node status")
    91  		return err
    92  	}
    93  
    94  	return nil
    95  }
    96  
    97  // specPVC updates Bitcoin node persistent volume claim
    98  func (r *NodeReconciler) specPVC(node *bitcoinv1alpha1.Node, pvc *corev1.PersistentVolumeClaim) {
    99  	request := corev1.ResourceList{
   100  		corev1.ResourceStorage: resource.MustParse(node.Spec.Storage),
   101  	}
   102  
   103  	// spec is immutable after creation except resources.requests for bound claims
   104  	if !pvc.CreationTimestamp.IsZero() {
   105  		pvc.Spec.Resources.Requests = request
   106  		return
   107  	}
   108  
   109  	pvc.ObjectMeta.Labels = node.Labels
   110  	pvc.Spec = corev1.PersistentVolumeClaimSpec{
   111  		AccessModes: []corev1.PersistentVolumeAccessMode{
   112  			corev1.ReadWriteOnce,
   113  		},
   114  		Resources: corev1.VolumeResourceRequirements{
   115  			Requests: request,
   116  		},
   117  	}
   118  }
   119  
   120  // specService updates Bitcoin node service spec
   121  func (r *NodeReconciler) specService(node *bitcoinv1alpha1.Node, svc *corev1.Service) {
   122  	labels := node.Labels
   123  
   124  	svc.ObjectMeta.Labels = labels
   125  
   126  	svc.Spec.Ports = []corev1.ServicePort{
   127  		{
   128  			Name:       "p2p",
   129  			Port:       int32(node.Spec.P2PPort),
   130  			TargetPort: intstr.FromString("p2p"),
   131  		},
   132  	}
   133  
   134  	if node.Spec.RPC {
   135  		svc.Spec.Ports = append(svc.Spec.Ports, corev1.ServicePort{
   136  			Name:       "rpc",
   137  			Port:       int32(node.Spec.RPCPort),
   138  			TargetPort: intstr.FromString("rpc"),
   139  		})
   140  	}
   141  
   142  	svc.Spec.Selector = labels
   143  }
   144  
   145  // specStatefulSet updates node statefulset spec
   146  func (r *NodeReconciler) specStatefulSet(node *bitcoinv1alpha1.Node, sts *appsv1.StatefulSet, homeDir string, env []corev1.EnvVar, cmd, args []string) error {
   147  
   148  	sts.ObjectMeta.Labels = node.Labels
   149  
   150  	ports := []corev1.ContainerPort{
   151  		{
   152  			Name:          "p2p",
   153  			ContainerPort: int32(node.Spec.P2PPort),
   154  		},
   155  	}
   156  
   157  	if node.Spec.RPC {
   158  		ports = append(ports, corev1.ContainerPort{
   159  			Name:          "rpc",
   160  			ContainerPort: int32(node.Spec.RPCPort),
   161  		})
   162  	}
   163  
   164  	replicas := int32(*node.Spec.Replicas)
   165  
   166  	sts.Spec = appsv1.StatefulSetSpec{
   167  		Selector: &metav1.LabelSelector{
   168  			MatchLabels: node.Labels,
   169  		},
   170  		Replicas:    &replicas,
   171  		ServiceName: node.Name,
   172  		Template: corev1.PodTemplateSpec{
   173  			ObjectMeta: metav1.ObjectMeta{
   174  				Labels: node.Labels,
   175  			},
   176  			Spec: corev1.PodSpec{
   177  				SecurityContext: shared.SecurityContext(),
   178  				Containers: []corev1.Container{
   179  					{
   180  						Name:    "node",
   181  						Image:   node.Spec.Image,
   182  						Command: cmd,
   183  						Args:    args,
   184  						Env:     env,
   185  						Ports:   ports,
   186  						VolumeMounts: []corev1.VolumeMount{
   187  							{
   188  								Name:      "data",
   189  								MountPath: shared.PathData(homeDir),
   190  							},
   191  						},
   192  						Resources: corev1.ResourceRequirements{
   193  							Requests: corev1.ResourceList{
   194  								corev1.ResourceCPU:    resource.MustParse(node.Spec.CPU),
   195  								corev1.ResourceMemory: resource.MustParse(node.Spec.Memory),
   196  							},
   197  							Limits: corev1.ResourceList{
   198  								corev1.ResourceCPU:    resource.MustParse(node.Spec.CPULimit),
   199  								corev1.ResourceMemory: resource.MustParse(node.Spec.MemoryLimit),
   200  							},
   201  						},
   202  					},
   203  				},
   204  				Volumes: []corev1.Volume{
   205  					{
   206  						Name: "data",
   207  						VolumeSource: corev1.VolumeSource{
   208  							PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
   209  								ClaimName: node.Name,
   210  							},
   211  						},
   212  					},
   213  				},
   214  			},
   215  		},
   216  	}
   217  
   218  	return nil
   219  }
   220  
   221  func (r *NodeReconciler) SetupWithManager(mgr ctrl.Manager) error {
   222  	pred := predicate.GenerationChangedPredicate{}
   223  	return ctrl.NewControllerManagedBy(mgr).
   224  		For(&bitcoinv1alpha1.Node{}).
   225  		WithEventFilter(pred).
   226  		Owns(&corev1.PersistentVolumeClaim{}).
   227  		Owns(&appsv1.StatefulSet{}).
   228  		Owns(&corev1.Service{}).
   229  		Complete(r)
   230  }