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

     1  package controllers
     2  
     3  import (
     4  	"context"
     5  
     6  	stacksClients "github.com/kotalco/kotal/clients/stacks"
     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  	stacksv1alpha1 "github.com/kotalco/kotal/apis/stacks/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=stacks.kotal.io,resources=nodes,verbs=get;list;watch;create;update;patch;delete
    27  // +kubebuilder:rbac:groups=stacks.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;configmaps,verbs=watch;get;create;update;list;delete
    30  
    31  func (r *NodeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) {
    32  	defer shared.IgnoreConflicts(&err)
    33  
    34  	var node stacksv1alpha1.Node
    35  
    36  	if err = r.Client.Get(ctx, req.NamespacedName, &node); err != nil {
    37  		err = client.IgnoreNotFound(err)
    38  		return
    39  	}
    40  
    41  	// default the node if webhooks are disabled
    42  	if !shared.IsWebhookEnabled() {
    43  		node.Default()
    44  	}
    45  
    46  	shared.UpdateLabels(&node, "stacks-node", string(node.Spec.Network))
    47  
    48  	// reconcile config map
    49  	if err = r.ReconcileOwned(ctx, &node, &corev1.ConfigMap{}, func(obj client.Object) error {
    50  		configToml, err := ConfigFromSpec(&node, r.Client)
    51  		if err != nil {
    52  			return err
    53  		}
    54  		r.specConfigmap(&node, obj.(*corev1.ConfigMap), configToml)
    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 := stacksClients.NewClient(&node)
    79  
    80  		homeDir := client.HomeDir()
    81  		cmd := client.Command()
    82  		args := client.Args()
    83  		args = append(args, node.Spec.ExtraArgs.Encode(false)...)
    84  		env := client.Env()
    85  
    86  		return r.specStatefulSet(&node, obj.(*appsv1.StatefulSet), homeDir, env, cmd, args)
    87  	}); err != nil {
    88  		return
    89  	}
    90  
    91  	if err = r.updateStatus(ctx, &node); err != nil {
    92  		return
    93  	}
    94  
    95  	return
    96  }
    97  
    98  // updateStatus updates Stacks node status
    99  func (r *NodeReconciler) updateStatus(ctx context.Context, node *stacksv1alpha1.Node) error {
   100  	node.Status.Client = "stacks"
   101  
   102  	if err := r.Status().Update(ctx, node); err != nil {
   103  		log.FromContext(ctx).Error(err, "unable to update node status")
   104  		return err
   105  	}
   106  
   107  	return nil
   108  }
   109  
   110  // specConfigmap updates node statefulset spec
   111  func (r *NodeReconciler) specConfigmap(node *stacksv1alpha1.Node, configmap *corev1.ConfigMap, configToml string) {
   112  	configmap.ObjectMeta.Labels = node.Labels
   113  
   114  	if configmap.Data == nil {
   115  		configmap.Data = map[string]string{}
   116  	}
   117  
   118  	configmap.Data["config.toml"] = configToml
   119  
   120  }
   121  
   122  // specPVC updates Stacks node persistent volume claim
   123  func (r *NodeReconciler) specPVC(node *stacksv1alpha1.Node, pvc *corev1.PersistentVolumeClaim) {
   124  	request := corev1.ResourceList{
   125  		corev1.ResourceStorage: resource.MustParse(node.Spec.Storage),
   126  	}
   127  
   128  	// spec is immutable after creation except resources.requests for bound claims
   129  	if !pvc.CreationTimestamp.IsZero() {
   130  		pvc.Spec.Resources.Requests = request
   131  		return
   132  	}
   133  
   134  	pvc.ObjectMeta.Labels = node.Labels
   135  	pvc.Spec = corev1.PersistentVolumeClaimSpec{
   136  		AccessModes: []corev1.PersistentVolumeAccessMode{
   137  			corev1.ReadWriteOnce,
   138  		},
   139  		Resources: corev1.VolumeResourceRequirements{
   140  			Requests: request,
   141  		},
   142  	}
   143  }
   144  
   145  // specService updates Bitcoin node service spec
   146  func (r *NodeReconciler) specService(node *stacksv1alpha1.Node, svc *corev1.Service) {
   147  	labels := node.Labels
   148  
   149  	svc.ObjectMeta.Labels = labels
   150  
   151  	svc.Spec.Ports = []corev1.ServicePort{
   152  		{
   153  			Name:       "p2p",
   154  			Port:       int32(node.Spec.P2PPort),
   155  			TargetPort: intstr.FromString("p2p"),
   156  		},
   157  		{
   158  			Name:       "rpc",
   159  			Port:       int32(node.Spec.RPCPort),
   160  			TargetPort: intstr.FromString("rpc"),
   161  		},
   162  	}
   163  
   164  	svc.Spec.Selector = labels
   165  }
   166  
   167  // specStatefulSet updates node statefulset spec
   168  func (r *NodeReconciler) specStatefulSet(node *stacksv1alpha1.Node, sts *appsv1.StatefulSet, homeDir string, env []corev1.EnvVar, cmd, args []string) error {
   169  
   170  	sts.ObjectMeta.Labels = node.Labels
   171  
   172  	ports := []corev1.ContainerPort{
   173  		{
   174  			Name:          "p2p",
   175  			ContainerPort: int32(node.Spec.P2PPort),
   176  		},
   177  		{
   178  			Name:          "rpc",
   179  			ContainerPort: int32(node.Spec.RPCPort),
   180  		},
   181  	}
   182  
   183  	replicas := int32(*node.Spec.Replicas)
   184  
   185  	sts.Spec = appsv1.StatefulSetSpec{
   186  		Selector: &metav1.LabelSelector{
   187  			MatchLabels: node.Labels,
   188  		},
   189  		ServiceName: node.Name,
   190  		Replicas:    &replicas,
   191  		Template: corev1.PodTemplateSpec{
   192  			ObjectMeta: metav1.ObjectMeta{
   193  				Labels: node.Labels,
   194  			},
   195  			Spec: corev1.PodSpec{
   196  				SecurityContext: shared.SecurityContext(),
   197  				Containers: []corev1.Container{
   198  					{
   199  						Name:    "node",
   200  						Image:   node.Spec.Image,
   201  						Command: cmd,
   202  						Args:    args,
   203  						Ports:   ports,
   204  						Env:     env,
   205  						Resources: corev1.ResourceRequirements{
   206  							Requests: corev1.ResourceList{
   207  								corev1.ResourceCPU:    resource.MustParse(node.Spec.CPU),
   208  								corev1.ResourceMemory: resource.MustParse(node.Spec.Memory),
   209  							},
   210  							Limits: corev1.ResourceList{
   211  								corev1.ResourceCPU:    resource.MustParse(node.Spec.CPULimit),
   212  								corev1.ResourceMemory: resource.MustParse(node.Spec.MemoryLimit),
   213  							},
   214  						},
   215  						VolumeMounts: []corev1.VolumeMount{
   216  							{
   217  								Name:      "data",
   218  								MountPath: shared.PathData(homeDir),
   219  							},
   220  							{
   221  								Name:      "config",
   222  								ReadOnly:  true,
   223  								MountPath: shared.PathConfig(homeDir),
   224  							},
   225  						},
   226  					},
   227  				},
   228  				Volumes: []corev1.Volume{
   229  					{
   230  						Name: "config",
   231  						VolumeSource: corev1.VolumeSource{
   232  							ConfigMap: &corev1.ConfigMapVolumeSource{
   233  								LocalObjectReference: corev1.LocalObjectReference{
   234  									Name: node.Name,
   235  								},
   236  							},
   237  						},
   238  					},
   239  					{
   240  						Name: "data",
   241  						VolumeSource: corev1.VolumeSource{
   242  							PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
   243  								ClaimName: node.Name,
   244  							},
   245  						},
   246  					},
   247  				},
   248  			},
   249  		},
   250  	}
   251  
   252  	return nil
   253  }
   254  
   255  func (r *NodeReconciler) SetupWithManager(mgr ctrl.Manager) error {
   256  	pred := predicate.GenerationChangedPredicate{}
   257  	return ctrl.NewControllerManagedBy(mgr).
   258  		For(&stacksv1alpha1.Node{}).
   259  		WithEventFilter(pred).
   260  		Owns(&appsv1.StatefulSet{}).
   261  		Owns(&corev1.ConfigMap{}).
   262  		Owns(&corev1.Service{}).
   263  		Complete(r)
   264  }