github.com/kotalco/kotal@v0.3.0/controllers/chainlink/node_controller.go (about) 1 package controllers 2 3 import ( 4 "context" 5 _ "embed" 6 "fmt" 7 8 chainlinkv1alpha1 "github.com/kotalco/kotal/apis/chainlink/v1alpha1" 9 chainlinkClients "github.com/kotalco/kotal/clients/chainlink" 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 "sigs.k8s.io/controller-runtime/pkg/log" 19 "sigs.k8s.io/controller-runtime/pkg/predicate" 20 ) 21 22 // NodeReconciler reconciles a Node object 23 type NodeReconciler struct { 24 shared.Reconciler 25 } 26 27 const ( 28 envApiEmail = "KOTAL_API_EMAIL" 29 ) 30 31 var ( 32 //go:embed copy_api_credentials.sh 33 CopyAPICredentials string 34 ) 35 36 // +kubebuilder:rbac:groups=chainlink.kotal.io,resources=nodes,verbs=get;list;watch;create;update;patch;delete 37 // +kubebuilder:rbac:groups=chainlink.kotal.io,resources=nodes/status,verbs=get;update;patch 38 // +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=watch;get;list;create;update;delete 39 // +kubebuilder:rbac:groups=core,resources=services;configmaps;persistentvolumeclaims,verbs=watch;get;create;update;list;delete 40 41 func (r *NodeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { 42 defer shared.IgnoreConflicts(&err) 43 44 var node chainlinkv1alpha1.Node 45 46 if err = r.Client.Get(ctx, req.NamespacedName, &node); err != nil { 47 err = client.IgnoreNotFound(err) 48 return 49 } 50 51 // default the node if webhooks are disabled 52 if !shared.IsWebhookEnabled() { 53 node.Default() 54 } 55 56 shared.UpdateLabels(&node, "chainlink", "") 57 58 // reconcile service 59 if err = r.ReconcileOwned(ctx, &node, &corev1.Service{}, func(obj client.Object) error { 60 r.specService(&node, obj.(*corev1.Service)) 61 return nil 62 }); err != nil { 63 return 64 } 65 66 // reconcile config map 67 if err = r.ReconcileOwned(ctx, &node, &corev1.ConfigMap{}, func(obj client.Object) error { 68 homeDir := chainlinkClients.NewClient(&node).HomeDir() 69 70 configToml, err := ConfigFromSpec(&node, homeDir) 71 if err != nil { 72 return err 73 } 74 75 secretsConfigToml, err := SecretsFromSpec(&node, homeDir, r.Client) 76 if err != nil { 77 return err 78 } 79 r.specConfigmap(&node, obj.(*corev1.ConfigMap), configToml, secretsConfigToml) 80 return nil 81 }); err != nil { 82 return 83 } 84 85 // reconcile persistent volume claim 86 if err = r.ReconcileOwned(ctx, &node, &corev1.PersistentVolumeClaim{}, func(obj client.Object) error { 87 r.specPVC(&node, obj.(*corev1.PersistentVolumeClaim)) 88 return nil 89 }); err != nil { 90 return 91 } 92 93 // reconcile stateful set 94 if err = r.ReconcileOwned(ctx, &node, &appsv1.StatefulSet{}, func(obj client.Object) error { 95 client := chainlinkClients.NewClient(&node) 96 97 command := client.Command() 98 args := client.Args() 99 args = append(args, node.Spec.ExtraArgs.Encode(false)...) 100 env := client.Env() 101 homeDir := client.HomeDir() 102 return r.specStatefulSet(&node, obj.(*appsv1.StatefulSet), homeDir, command, args, env) 103 }); err != nil { 104 return 105 } 106 107 if err = r.updateStatus(ctx, &node); err != nil { 108 return 109 } 110 111 return 112 } 113 114 // updateStatus updates chainlink node status 115 func (r *NodeReconciler) updateStatus(ctx context.Context, node *chainlinkv1alpha1.Node) error { 116 node.Status.Client = "chainlink" 117 118 if err := r.Status().Update(ctx, node); err != nil { 119 log.FromContext(ctx).Error(err, "unable to update node status") 120 return err 121 } 122 123 return nil 124 } 125 126 // specService updates node service spec 127 func (r *NodeReconciler) specService(node *chainlinkv1alpha1.Node, svc *corev1.Service) { 128 labels := node.Labels 129 130 svc.ObjectMeta.Labels = labels 131 132 svc.Spec.Ports = []corev1.ServicePort{ 133 { 134 Name: "p2p", 135 Port: int32(node.Spec.P2PPort), 136 TargetPort: intstr.FromString("p2p"), 137 }, 138 } 139 140 if node.Spec.API { 141 svc.Spec.Ports = append(svc.Spec.Ports, corev1.ServicePort{ 142 Name: "api", 143 Port: int32(node.Spec.APIPort), 144 TargetPort: intstr.FromString("api"), 145 }) 146 } 147 148 if node.Spec.TLSPort != 0 { 149 svc.Spec.Ports = append(svc.Spec.Ports, corev1.ServicePort{ 150 Name: "tls", 151 Port: int32(node.Spec.TLSPort), 152 TargetPort: intstr.FromString("tls"), 153 }) 154 } 155 156 svc.Spec.Selector = labels 157 } 158 159 // specConfigmap updates chainlink node configmap spec 160 func (r *NodeReconciler) specConfigmap(node *chainlinkv1alpha1.Node, config *corev1.ConfigMap, configToml, secretsConfigToml string) { 161 config.ObjectMeta.Labels = node.Labels 162 163 if config.Data == nil { 164 config.Data = make(map[string]string) 165 } 166 167 config.Data["config.toml"] = configToml 168 config.Data["secrets.toml"] = secretsConfigToml 169 config.Data["copy_api_credentials.sh"] = CopyAPICredentials 170 } 171 172 func (r *NodeReconciler) createVolumes(node *chainlinkv1alpha1.Node) []corev1.Volume { 173 volumes := []corev1.Volume{} 174 175 // data volume 176 volumes = append(volumes, corev1.Volume{ 177 Name: "data", 178 VolumeSource: corev1.VolumeSource{ 179 PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ 180 ClaimName: node.Name, 181 }, 182 }, 183 }) 184 185 // config volume 186 volumes = append(volumes, corev1.Volume{ 187 Name: "config", 188 VolumeSource: corev1.VolumeSource{ 189 ConfigMap: &corev1.ConfigMapVolumeSource{ 190 LocalObjectReference: corev1.LocalObjectReference{ 191 Name: node.Name, 192 }, 193 }, 194 }, 195 }) 196 197 // projected volume sources 198 sources := []corev1.VolumeProjection{ 199 { 200 Secret: &corev1.SecretProjection{ 201 LocalObjectReference: corev1.LocalObjectReference{ 202 Name: node.Spec.APICredentials.PasswordSecretName, 203 }, 204 Items: []corev1.KeyToPath{ 205 { 206 Key: "password", 207 Path: "api-password", 208 }, 209 }, 210 }, 211 }, 212 } 213 214 if node.Spec.CertSecretName != "" { 215 sources = append(sources, corev1.VolumeProjection{ 216 Secret: &corev1.SecretProjection{ 217 LocalObjectReference: corev1.LocalObjectReference{ 218 Name: node.Spec.CertSecretName, 219 }, 220 }, 221 }) 222 } 223 224 // secrets volume 225 volumes = append(volumes, corev1.Volume{ 226 Name: "secrets", 227 VolumeSource: corev1.VolumeSource{ 228 Projected: &corev1.ProjectedVolumeSource{ 229 Sources: sources, 230 }, 231 }, 232 }) 233 234 return volumes 235 } 236 237 func (r *NodeReconciler) createVolumeMounts(node *chainlinkv1alpha1.Node, homeDir string) []corev1.VolumeMount { 238 // chainlink chmod the root dir 239 // we mount data volume at home dir 240 // chainlink root dir will be mounted at $data/kotal-data 241 return []corev1.VolumeMount{ 242 { 243 Name: "data", 244 MountPath: homeDir, 245 }, 246 { 247 Name: "config", 248 MountPath: shared.PathConfig(homeDir), 249 }, 250 { 251 Name: "secrets", 252 MountPath: shared.PathSecrets(homeDir), 253 }, 254 } 255 } 256 257 // specStatefulSet updates node statefulset spec 258 func (r *NodeReconciler) specStatefulSet(node *chainlinkv1alpha1.Node, sts *appsv1.StatefulSet, homeDir string, command, args []string, env []corev1.EnvVar) error { 259 260 sts.ObjectMeta.Labels = node.Labels 261 262 ports := []corev1.ContainerPort{ 263 { 264 Name: "p2p", 265 ContainerPort: int32(node.Spec.P2PPort), 266 }, 267 } 268 269 if node.Spec.API { 270 ports = append(ports, corev1.ContainerPort{ 271 Name: "api", 272 ContainerPort: int32(node.Spec.APIPort), 273 }) 274 } 275 276 if node.Spec.TLSPort != 0 { 277 ports = append(ports, corev1.ContainerPort{ 278 Name: "tls", 279 ContainerPort: int32(node.Spec.TLSPort), 280 }) 281 } 282 283 replicas := int32(*node.Spec.Replicas) 284 285 sts.Spec = appsv1.StatefulSetSpec{ 286 Selector: &metav1.LabelSelector{ 287 MatchLabels: node.Labels, 288 }, 289 Replicas: &replicas, 290 ServiceName: node.Name, 291 Template: corev1.PodTemplateSpec{ 292 ObjectMeta: metav1.ObjectMeta{ 293 Labels: node.Labels, 294 }, 295 Spec: corev1.PodSpec{ 296 SecurityContext: shared.SecurityContext(), 297 InitContainers: []corev1.Container{ 298 { 299 Name: "copy-api-credentials", 300 Image: shared.BusyboxImage, 301 Command: []string{"/bin/sh"}, 302 Env: []corev1.EnvVar{ 303 { 304 Name: shared.EnvDataPath, 305 Value: shared.PathData(homeDir), 306 }, 307 { 308 Name: envApiEmail, 309 Value: node.Spec.APICredentials.Email, 310 }, 311 { 312 Name: shared.EnvSecretsPath, 313 Value: shared.PathSecrets(homeDir), 314 }, 315 }, 316 Args: []string{fmt.Sprintf("%s/copy_api_credentials.sh", shared.PathConfig(homeDir))}, 317 VolumeMounts: r.createVolumeMounts(node, homeDir), 318 }, 319 }, 320 Containers: []corev1.Container{ 321 { 322 Name: "node", 323 Image: node.Spec.Image, 324 Command: command, 325 Args: args, 326 Env: env, 327 Ports: ports, 328 Resources: corev1.ResourceRequirements{ 329 Requests: corev1.ResourceList{ 330 corev1.ResourceCPU: resource.MustParse(node.Spec.Resources.CPU), 331 corev1.ResourceMemory: resource.MustParse(node.Spec.Resources.Memory), 332 }, 333 Limits: corev1.ResourceList{ 334 corev1.ResourceCPU: resource.MustParse(node.Spec.Resources.CPULimit), 335 corev1.ResourceMemory: resource.MustParse(node.Spec.Resources.MemoryLimit), 336 }, 337 }, 338 VolumeMounts: r.createVolumeMounts(node, homeDir), 339 }, 340 }, 341 Volumes: r.createVolumes(node), 342 }, 343 }, 344 } 345 346 return nil 347 } 348 349 // specPVC updates chainlink persistent volume claim 350 func (r *NodeReconciler) specPVC(node *chainlinkv1alpha1.Node, pvc *corev1.PersistentVolumeClaim) { 351 request := corev1.ResourceList{ 352 corev1.ResourceStorage: resource.MustParse(node.Spec.Storage), 353 } 354 355 // spec is immutable after creation except resources.requests for bound claims 356 if !pvc.CreationTimestamp.IsZero() { 357 pvc.Spec.Resources.Requests = request 358 return 359 } 360 361 pvc.ObjectMeta.Labels = node.Labels 362 pvc.Spec = corev1.PersistentVolumeClaimSpec{ 363 AccessModes: []corev1.PersistentVolumeAccessMode{ 364 corev1.ReadWriteOnce, 365 }, 366 Resources: corev1.VolumeResourceRequirements{ 367 Requests: request, 368 }, 369 StorageClassName: node.Spec.StorageClass, 370 } 371 } 372 373 func (r *NodeReconciler) SetupWithManager(mgr ctrl.Manager) error { 374 pred := predicate.GenerationChangedPredicate{} 375 return ctrl.NewControllerManagedBy(mgr). 376 For(&chainlinkv1alpha1.Node{}). 377 WithEventFilter(pred). 378 Owns(&appsv1.StatefulSet{}). 379 Owns(&corev1.ConfigMap{}). 380 Owns(&corev1.Service{}). 381 Owns(&corev1.PersistentVolumeClaim{}). 382 Complete(r) 383 }