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 }