github.com/kotalco/kotal@v0.3.0/controllers/ipfs/peer_controller.go (about) 1 package controllers 2 3 import ( 4 "context" 5 _ "embed" 6 "fmt" 7 "strings" 8 9 appsv1 "k8s.io/api/apps/v1" 10 corev1 "k8s.io/api/core/v1" 11 "k8s.io/apimachinery/pkg/api/resource" 12 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 "k8s.io/apimachinery/pkg/util/intstr" 14 ctrl "sigs.k8s.io/controller-runtime" 15 "sigs.k8s.io/controller-runtime/pkg/client" 16 "sigs.k8s.io/controller-runtime/pkg/log" 17 "sigs.k8s.io/controller-runtime/pkg/predicate" 18 19 ipfsv1alpha1 "github.com/kotalco/kotal/apis/ipfs/v1alpha1" 20 ipfsClients "github.com/kotalco/kotal/clients/ipfs" 21 "github.com/kotalco/kotal/controllers/shared" 22 ) 23 24 // PeerReconciler reconciles a Peer object 25 type PeerReconciler struct { 26 shared.Reconciler 27 } 28 29 var ( 30 //go:embed init_ipfs_config.sh 31 initIPFSConfigScript string 32 //go:embed copy_swarm_key.sh 33 copySwarmKeyScript string 34 //go:embed config_ipfs.sh 35 configIPFSScript string 36 ) 37 38 // +kubebuilder:rbac:groups=ipfs.kotal.io,resources=peers,verbs=get;list;watch;create;update;patch;delete 39 // +kubebuilder:rbac:groups=ipfs.kotal.io,resources=peers/status,verbs=get;update;patch 40 // +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=watch;get;list;create;update;delete 41 // +kubebuilder:rbac:groups=core,resources=services;configmaps;persistentvolumeclaims,verbs=watch;get;create;update;list;delete 42 43 func (r *PeerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { 44 defer shared.IgnoreConflicts(&err) 45 46 var peer ipfsv1alpha1.Peer 47 48 if err = r.Client.Get(ctx, req.NamespacedName, &peer); err != nil { 49 err = client.IgnoreNotFound(err) 50 return 51 } 52 53 // default the peer if webhooks are disabled 54 if !shared.IsWebhookEnabled() { 55 peer.Default() 56 } 57 58 shared.UpdateLabels(&peer, "kubo", "") 59 60 // reconcile config map 61 if err = r.ReconcileOwned(ctx, &peer, &corev1.ConfigMap{}, func(obj client.Object) error { 62 r.specConfigmap(&peer, obj.(*corev1.ConfigMap)) 63 return nil 64 }); err != nil { 65 return 66 } 67 68 // reconcile service 69 if err = r.ReconcileOwned(ctx, &peer, &corev1.Service{}, func(obj client.Object) error { 70 r.specService(&peer, obj.(*corev1.Service)) 71 return nil 72 }); err != nil { 73 return 74 } 75 76 // reconcile persistent volume claim 77 if err = r.ReconcileOwned(ctx, &peer, &corev1.PersistentVolumeClaim{}, func(obj client.Object) error { 78 r.specPVC(&peer, obj.(*corev1.PersistentVolumeClaim)) 79 return nil 80 }); err != nil { 81 return 82 } 83 84 // reconcile stateful set 85 if err = r.ReconcileOwned(ctx, &peer, &appsv1.StatefulSet{}, func(obj client.Object) error { 86 client, err := ipfsClients.NewClient(&peer) 87 if err != nil { 88 return err 89 } 90 91 command := client.Command() 92 env := client.Env() 93 args := client.Args() 94 args = append(args, peer.Spec.ExtraArgs.Encode(false)...) 95 homeDir := client.HomeDir() 96 97 r.specStatefulSet(&peer, obj.(*appsv1.StatefulSet), homeDir, env, command, args) 98 return nil 99 }); err != nil { 100 return 101 } 102 103 if err = r.updateStatus(ctx, &peer); err != nil { 104 return 105 } 106 107 return 108 } 109 110 // updateStatus updates ipfs peer status 111 func (r *PeerReconciler) updateStatus(ctx context.Context, peer *ipfsv1alpha1.Peer) error { 112 // TODO: update after multi-client support 113 peer.Status.Client = "kubo" 114 115 if err := r.Status().Update(ctx, peer); err != nil { 116 log.FromContext(ctx).Error(err, "unable to update peer status") 117 return err 118 } 119 120 return nil 121 } 122 123 // specService updates ipfs peer service spec 124 func (r *PeerReconciler) specService(peer *ipfsv1alpha1.Peer, svc *corev1.Service) { 125 labels := peer.Labels 126 127 svc.ObjectMeta.Labels = labels 128 129 ports := []corev1.ServicePort{ 130 { 131 Name: "swarm", 132 Port: 4001, 133 TargetPort: intstr.FromString("swarm"), 134 }, 135 { 136 Name: "swarm-udp", 137 Port: 4001, 138 TargetPort: intstr.FromString("swarm-udp"), 139 Protocol: corev1.ProtocolUDP, 140 }, 141 } 142 143 if peer.Spec.API { 144 ports = append(ports, corev1.ServicePort{ 145 Name: "api", 146 Port: int32(peer.Spec.APIPort), 147 TargetPort: intstr.FromString("api"), 148 }) 149 } 150 151 if peer.Spec.Gateway { 152 ports = append(ports, corev1.ServicePort{ 153 Name: "gateway", 154 Port: int32(peer.Spec.GatewayPort), 155 TargetPort: intstr.FromString("gateway"), 156 }) 157 } 158 159 svc.Spec.Ports = ports 160 161 svc.Spec.Selector = labels 162 } 163 164 // specConfigmap updates ipfs peer config spec 165 func (r *PeerReconciler) specConfigmap(peer *ipfsv1alpha1.Peer, config *corev1.ConfigMap) { 166 config.ObjectMeta.Labels = peer.Labels 167 if config.Data == nil { 168 config.Data = make(map[string]string) 169 } 170 config.Data["init_ipfs_config.sh"] = initIPFSConfigScript 171 config.Data["copy_swarm_key.sh"] = copySwarmKeyScript 172 config.Data["config_ipfs.sh"] = configIPFSScript 173 } 174 175 // specPVC updates ipfs peer persistent volume claim 176 func (r *PeerReconciler) specPVC(peer *ipfsv1alpha1.Peer, pvc *corev1.PersistentVolumeClaim) { 177 request := corev1.ResourceList{ 178 corev1.ResourceStorage: resource.MustParse(peer.Spec.Resources.Storage), 179 } 180 181 // spec is immutable after creation except resources.requests for bound claims 182 if !pvc.CreationTimestamp.IsZero() { 183 pvc.Spec.Resources.Requests = request 184 return 185 } 186 187 pvc.ObjectMeta.Labels = peer.Labels 188 pvc.Spec = corev1.PersistentVolumeClaimSpec{ 189 AccessModes: []corev1.PersistentVolumeAccessMode{ 190 corev1.ReadWriteOnce, 191 }, 192 Resources: corev1.VolumeResourceRequirements{ 193 Requests: request, 194 }, 195 StorageClassName: peer.Spec.Resources.StorageClass, 196 } 197 } 198 199 // specStatefulSet updates ipfs peer statefulset spec 200 func (r *PeerReconciler) specStatefulSet(peer *ipfsv1alpha1.Peer, sts *appsv1.StatefulSet, homeDir string, env []corev1.EnvVar, command, args []string) { 201 labels := peer.Labels 202 203 sts.ObjectMeta.Labels = labels 204 205 volumes := []corev1.Volume{ 206 { 207 Name: "data", 208 VolumeSource: corev1.VolumeSource{ 209 PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ 210 ClaimName: peer.Name, 211 }, 212 }, 213 }, 214 { 215 Name: "config", 216 VolumeSource: corev1.VolumeSource{ 217 ConfigMap: &corev1.ConfigMapVolumeSource{ 218 LocalObjectReference: corev1.LocalObjectReference{ 219 Name: peer.Name, 220 }, 221 }, 222 }, 223 }, 224 } 225 226 volumeMounts := []corev1.VolumeMount{ 227 { 228 Name: "data", 229 MountPath: shared.PathData(homeDir), 230 }, 231 { 232 Name: "config", 233 MountPath: shared.PathConfig(homeDir), 234 }, 235 } 236 237 initContainers := []corev1.Container{} 238 239 // copy swarm key before init ipfs 240 if peer.Spec.SwarmKeySecretName != "" { 241 volumes = append(volumes, corev1.Volume{ 242 Name: "swarm-key", 243 VolumeSource: corev1.VolumeSource{ 244 Secret: &corev1.SecretVolumeSource{ 245 SecretName: peer.Spec.SwarmKeySecretName, 246 }, 247 }, 248 }) 249 250 volumeMounts = append(volumeMounts, corev1.VolumeMount{ 251 Name: "swarm-key", 252 MountPath: shared.PathSecrets(homeDir), 253 }) 254 255 initContainers = append(initContainers, corev1.Container{ 256 Name: "copy-swarm-key", 257 Image: shared.BusyboxImage, 258 Env: []corev1.EnvVar{ 259 { 260 Name: ipfsClients.EnvIPFSPath, 261 Value: shared.PathData(homeDir), 262 }, 263 { 264 Name: shared.EnvSecretsPath, 265 Value: shared.PathSecrets(homeDir), 266 }, 267 }, 268 Command: []string{"/bin/sh"}, 269 Args: []string{ 270 fmt.Sprintf("%s/copy_swarm_key.sh", shared.PathConfig(homeDir)), 271 }, 272 VolumeMounts: volumeMounts, 273 }) 274 275 } 276 277 // init ipfs config 278 initProfiles := []string{} 279 for _, profile := range peer.Spec.InitProfiles { 280 initProfiles = append(initProfiles, string(profile)) 281 } 282 initContainers = append(initContainers, corev1.Container{ 283 Name: "init-ipfs", 284 Image: peer.Spec.Image, 285 Env: []corev1.EnvVar{ 286 { 287 Name: ipfsClients.EnvIPFSPath, 288 Value: shared.PathData(homeDir), 289 }, 290 { 291 Name: ipfsClients.EnvIPFSInitProfiles, 292 Value: strings.Join(initProfiles, ","), 293 }, 294 }, 295 Command: []string{"/bin/sh"}, 296 Args: []string{ 297 fmt.Sprintf("%s/init_ipfs_config.sh", shared.PathConfig(homeDir)), 298 }, 299 VolumeMounts: volumeMounts, 300 }) 301 302 // init ipfs config 303 profiles := []string{} 304 for _, profile := range peer.Spec.Profiles { 305 profiles = append(profiles, string(profile)) 306 } 307 // config ipfs 308 initContainers = append(initContainers, corev1.Container{ 309 Name: "config-ipfs", 310 Image: peer.Spec.Image, 311 Env: []corev1.EnvVar{ 312 { 313 Name: ipfsClients.EnvIPFSPath, 314 Value: shared.PathData(homeDir), 315 }, 316 { 317 Name: ipfsClients.EnvIPFSAPIPort, 318 Value: fmt.Sprintf("%d", peer.Spec.APIPort), 319 }, 320 { 321 Name: ipfsClients.EnvIPFSAPIHost, 322 Value: shared.Host(peer.Spec.API), 323 }, 324 { 325 Name: ipfsClients.EnvIPFSGatewayPort, 326 Value: fmt.Sprintf("%d", peer.Spec.GatewayPort), 327 }, 328 { 329 Name: ipfsClients.EnvIPFSGatewayHost, 330 Value: shared.Host(peer.Spec.Gateway), 331 }, 332 { 333 Name: ipfsClients.EnvIPFSProfiles, 334 Value: strings.Join(profiles, ";"), 335 }, 336 }, 337 Command: []string{"/bin/sh"}, 338 Args: []string{ 339 fmt.Sprintf("%s/config_ipfs.sh", shared.PathConfig(homeDir)), 340 }, 341 VolumeMounts: volumeMounts, 342 }) 343 344 ports := []corev1.ContainerPort{ 345 { 346 Name: "swarm", 347 ContainerPort: 4001, 348 }, 349 { 350 Name: "swarm-udp", 351 ContainerPort: 4001, 352 Protocol: corev1.ProtocolUDP, 353 }, 354 } 355 356 if peer.Spec.API { 357 ports = append(ports, corev1.ContainerPort{ 358 Name: "api", 359 ContainerPort: int32(peer.Spec.APIPort), 360 }) 361 } 362 363 if peer.Spec.Gateway { 364 ports = append(ports, corev1.ContainerPort{ 365 Name: "gateway", 366 ContainerPort: int32(peer.Spec.GatewayPort), 367 }) 368 } 369 370 replicas := int32(*peer.Spec.Replicas) 371 372 sts.Spec = appsv1.StatefulSetSpec{ 373 Selector: &metav1.LabelSelector{ 374 MatchLabels: labels, 375 }, 376 Replicas: &replicas, 377 Template: corev1.PodTemplateSpec{ 378 ObjectMeta: metav1.ObjectMeta{ 379 Labels: labels, 380 }, 381 Spec: corev1.PodSpec{ 382 SecurityContext: shared.SecurityContext(), 383 InitContainers: initContainers, 384 Containers: []corev1.Container{ 385 { 386 Name: "peer", 387 Image: peer.Spec.Image, 388 Env: env, 389 Command: command, 390 Args: args, 391 Ports: ports, 392 VolumeMounts: volumeMounts, 393 Resources: corev1.ResourceRequirements{ 394 Requests: corev1.ResourceList{ 395 corev1.ResourceCPU: resource.MustParse(peer.Spec.Resources.CPU), 396 corev1.ResourceMemory: resource.MustParse(peer.Spec.Resources.Memory), 397 }, 398 Limits: corev1.ResourceList{ 399 corev1.ResourceCPU: resource.MustParse(peer.Spec.Resources.CPULimit), 400 corev1.ResourceMemory: resource.MustParse(peer.Spec.Resources.MemoryLimit), 401 }, 402 }, 403 }, 404 }, 405 Volumes: volumes, 406 }, 407 }, 408 } 409 } 410 411 // SetupWithManager registers the controller to be started with the given manager 412 func (r *PeerReconciler) SetupWithManager(mgr ctrl.Manager) error { 413 pred := predicate.GenerationChangedPredicate{} 414 return ctrl.NewControllerManagedBy(mgr). 415 For(&ipfsv1alpha1.Peer{}). 416 WithEventFilter(pred). 417 Owns(&appsv1.StatefulSet{}). 418 Owns(&corev1.PersistentVolumeClaim{}). 419 Owns(&corev1.ConfigMap{}). 420 Owns(&corev1.Service{}). 421 Complete(r) 422 }