github.com/kotalco/kotal@v0.3.0/controllers/polkadot/node_controller.go (about) 1 package controllers 2 3 import ( 4 "context" 5 _ "embed" 6 "fmt" 7 8 polkadotv1alpha1 "github.com/kotalco/kotal/apis/polkadot/v1alpha1" 9 polkadotClients "github.com/kotalco/kotal/clients/polkadot" 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 ) 19 20 // NodeReconciler reconciles a Node object 21 type NodeReconciler struct { 22 shared.Reconciler 23 } 24 25 var ( 26 //go:embed convert_node_private_key.sh 27 convertNodePrivateKeyScript string 28 ) 29 30 // +kubebuilder:rbac:groups=polkadot.kotal.io,resources=nodes,verbs=get;list;watch;create;update;patch;delete 31 // +kubebuilder:rbac:groups=polkadot.kotal.io,resources=nodes/status,verbs=get;update;patch 32 // +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=watch;get;list;create;update;delete 33 // +kubebuilder:rbac:groups=core,resources=services;configmaps;persistentvolumeclaims,verbs=watch;get;create;update;list;delete 34 35 func (r *NodeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { 36 defer shared.IgnoreConflicts(&err) 37 38 var node polkadotv1alpha1.Node 39 40 if err = r.Client.Get(ctx, req.NamespacedName, &node); err != nil { 41 err = client.IgnoreNotFound(err) 42 return 43 } 44 45 // default the node if webhooks are disabled 46 if !shared.IsWebhookEnabled() { 47 node.Default() 48 } 49 50 shared.UpdateLabels(&node, "polkadot", node.Spec.Network) 51 52 // reconcile config map 53 if err = r.ReconcileOwned(ctx, &node, &corev1.ConfigMap{}, func(obj client.Object) error { 54 r.specConfigmap(&node, obj.(*corev1.ConfigMap)) 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 := polkadotClients.NewClient(&node) 79 args := client.Args() 80 args = append(args, node.Spec.ExtraArgs.Encode(false)...) 81 homeDir := client.HomeDir() 82 83 return r.specStatefulSet(&node, obj.(*appsv1.StatefulSet), homeDir, args) 84 }); err != nil { 85 return 86 } 87 88 return 89 } 90 91 // specConfigmap updates polkadot node configmap spec 92 func (r *NodeReconciler) specConfigmap(node *polkadotv1alpha1.Node, config *corev1.ConfigMap) { 93 config.ObjectMeta.Labels = node.Labels 94 95 if config.Data == nil { 96 config.Data = make(map[string]string) 97 } 98 99 config.Data["convert_node_private_key.sh"] = convertNodePrivateKeyScript 100 } 101 102 // specService updates polkadot node service spec 103 func (r *NodeReconciler) specService(node *polkadotv1alpha1.Node, svc *corev1.Service) { 104 labels := node.Labels 105 106 svc.ObjectMeta.Labels = labels 107 108 svc.Spec.Ports = []corev1.ServicePort{ 109 { 110 Name: "p2p", 111 Port: int32(node.Spec.P2PPort), 112 TargetPort: intstr.FromString("p2p"), 113 }, 114 } 115 116 if node.Spec.Prometheus { 117 svc.Spec.Ports = append(svc.Spec.Ports, corev1.ServicePort{ 118 Name: "prometheus", 119 Port: int32(node.Spec.PrometheusPort), 120 TargetPort: intstr.FromString("prometheus"), 121 }) 122 } 123 124 if node.Spec.RPC { 125 svc.Spec.Ports = append(svc.Spec.Ports, corev1.ServicePort{ 126 Name: "rpc", 127 Port: int32(node.Spec.RPCPort), 128 TargetPort: intstr.FromString("rpc"), 129 }) 130 } 131 132 if node.Spec.WS { 133 svc.Spec.Ports = append(svc.Spec.Ports, corev1.ServicePort{ 134 Name: "ws", 135 Port: int32(node.Spec.WSPort), 136 TargetPort: intstr.FromString("ws"), 137 }) 138 } 139 140 svc.Spec.Selector = labels 141 } 142 143 // nodeVolumes returns node volumes 144 func (r *NodeReconciler) nodeVolumes(node *polkadotv1alpha1.Node) (volumes []corev1.Volume) { 145 dataVolume := corev1.Volume{ 146 Name: "data", 147 VolumeSource: corev1.VolumeSource{ 148 PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ 149 ClaimName: node.Name, 150 }, 151 }, 152 } 153 volumes = append(volumes, dataVolume) 154 155 configVolume := corev1.Volume{ 156 Name: "config", 157 VolumeSource: corev1.VolumeSource{ 158 ConfigMap: &corev1.ConfigMapVolumeSource{ 159 LocalObjectReference: corev1.LocalObjectReference{ 160 Name: node.Name, 161 }, 162 }, 163 }, 164 } 165 volumes = append(volumes, configVolume) 166 167 if node.Spec.NodePrivateKeySecretName != "" { 168 secretVolume := corev1.Volume{ 169 Name: "secret", 170 VolumeSource: corev1.VolumeSource{ 171 Secret: &corev1.SecretVolumeSource{ 172 SecretName: node.Spec.NodePrivateKeySecretName, 173 Items: []corev1.KeyToPath{ 174 { 175 Key: "key", 176 Path: "nodekey", 177 }, 178 }, 179 }, 180 }, 181 } 182 volumes = append(volumes, secretVolume) 183 } 184 185 return 186 } 187 188 // nodeVolumeMounts returns node volume mounts 189 func (r *NodeReconciler) nodeVolumeMounts(node *polkadotv1alpha1.Node, homeDir string) (mounts []corev1.VolumeMount) { 190 dataMount := corev1.VolumeMount{ 191 Name: "data", 192 MountPath: shared.PathData(homeDir), 193 } 194 mounts = append(mounts, dataMount) 195 196 configMount := corev1.VolumeMount{ 197 Name: "config", 198 MountPath: shared.PathConfig(homeDir), 199 } 200 mounts = append(mounts, configMount) 201 202 if node.Spec.NodePrivateKeySecretName != "" { 203 secretMount := corev1.VolumeMount{ 204 Name: "secret", 205 MountPath: shared.PathSecrets(homeDir), 206 } 207 mounts = append(mounts, secretMount) 208 } 209 return 210 } 211 212 // specStatefulSet updates node statefulset spec 213 func (r *NodeReconciler) specStatefulSet(node *polkadotv1alpha1.Node, sts *appsv1.StatefulSet, homeDir string, args []string) error { 214 215 sts.ObjectMeta.Labels = node.Labels 216 217 var initContainers []corev1.Container 218 219 if node.Spec.NodePrivateKeySecretName != "" { 220 convertEnodePrivateKey := corev1.Container{ 221 Name: "convert-node-private-key", 222 Image: shared.BusyboxImage, 223 Env: []corev1.EnvVar{ 224 { 225 Name: shared.EnvDataPath, 226 Value: shared.PathData(homeDir), 227 }, 228 { 229 Name: shared.EnvSecretsPath, 230 Value: shared.PathSecrets(homeDir), 231 }, 232 }, 233 Command: []string{"/bin/sh"}, 234 Args: []string{fmt.Sprintf("%s/convert_node_private_key.sh", shared.PathConfig(homeDir))}, 235 VolumeMounts: r.nodeVolumeMounts(node, homeDir), 236 } 237 initContainers = append(initContainers, convertEnodePrivateKey) 238 } 239 240 ports := []corev1.ContainerPort{ 241 { 242 Name: "p2p", 243 ContainerPort: int32(node.Spec.P2PPort), 244 }, 245 } 246 247 if node.Spec.Prometheus { 248 ports = append(ports, corev1.ContainerPort{ 249 Name: "prometheus", 250 ContainerPort: int32(node.Spec.PrometheusPort), 251 }) 252 } 253 254 if node.Spec.RPC { 255 ports = append(ports, corev1.ContainerPort{ 256 Name: "rpc", 257 ContainerPort: int32(node.Spec.RPCPort), 258 }) 259 } 260 261 if node.Spec.WS { 262 ports = append(ports, corev1.ContainerPort{ 263 Name: "ws", 264 ContainerPort: int32(node.Spec.RPCPort), 265 }) 266 } 267 268 replicas := int32(*node.Spec.Replicas) 269 270 sts.Spec = appsv1.StatefulSetSpec{ 271 Selector: &metav1.LabelSelector{ 272 MatchLabels: node.Labels, 273 }, 274 ServiceName: node.Name, 275 Replicas: &replicas, 276 Template: corev1.PodTemplateSpec{ 277 ObjectMeta: metav1.ObjectMeta{ 278 Labels: node.Labels, 279 }, 280 Spec: corev1.PodSpec{ 281 InitContainers: initContainers, 282 SecurityContext: shared.SecurityContext(), 283 Containers: []corev1.Container{ 284 { 285 Name: "node", 286 Image: node.Spec.Image, 287 Args: args, 288 Ports: ports, 289 VolumeMounts: r.nodeVolumeMounts(node, homeDir), 290 Resources: corev1.ResourceRequirements{ 291 Requests: map[corev1.ResourceName]resource.Quantity{ 292 corev1.ResourceCPU: resource.MustParse(node.Spec.CPU), 293 corev1.ResourceMemory: resource.MustParse(node.Spec.Memory), 294 }, 295 Limits: map[corev1.ResourceName]resource.Quantity{ 296 corev1.ResourceCPU: resource.MustParse(node.Spec.CPULimit), 297 corev1.ResourceMemory: resource.MustParse(node.Spec.MemoryLimit), 298 }, 299 }, 300 }, 301 }, 302 Volumes: r.nodeVolumes(node), 303 }, 304 }, 305 } 306 307 return nil 308 } 309 310 // specPVC updates ipfs peer persistent volume claim 311 func (r *NodeReconciler) specPVC(node *polkadotv1alpha1.Node, pvc *corev1.PersistentVolumeClaim) { 312 request := corev1.ResourceList{ 313 corev1.ResourceStorage: resource.MustParse(node.Spec.Storage), 314 } 315 316 // spec is immutable after creation except resources.requests for bound claims 317 if !pvc.CreationTimestamp.IsZero() { 318 pvc.Spec.Resources.Requests = request 319 return 320 } 321 322 pvc.ObjectMeta.Labels = node.Labels 323 pvc.Spec = corev1.PersistentVolumeClaimSpec{ 324 AccessModes: []corev1.PersistentVolumeAccessMode{ 325 corev1.ReadWriteOnce, 326 }, 327 Resources: corev1.VolumeResourceRequirements{ 328 Requests: request, 329 }, 330 StorageClassName: node.Spec.StorageClass, 331 } 332 } 333 334 func (r *NodeReconciler) SetupWithManager(mgr ctrl.Manager) error { 335 return ctrl.NewControllerManagedBy(mgr). 336 For(&polkadotv1alpha1.Node{}). 337 Owns(&corev1.PersistentVolumeClaim{}). 338 Owns(&corev1.Service{}). 339 Owns(&corev1.ConfigMap{}). 340 Owns(&appsv1.StatefulSet{}). 341 Complete(r) 342 }