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 }