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