github.com/kotalco/kotal@v0.3.0/controllers/ipfs/cluster_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 // ClusterPeerReconciler reconciles a ClusterPeer object 25 type ClusterPeerReconciler struct { 26 shared.Reconciler 27 } 28 29 var ( 30 //go:embed init_ipfs_cluster_config.sh 31 initIPFSClusterConfig string 32 ) 33 34 // +kubebuilder:rbac:groups=ipfs.kotal.io,resources=clusterpeers,verbs=get;list;watch;create;update;patch;delete 35 // +kubebuilder:rbac:groups=ipfs.kotal.io,resources=clusterpeers/status,verbs=get;update;patch 36 // +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=watch;get;list;create;update;delete 37 // +kubebuilder:rbac:groups=core,resources=configmaps;services;persistentvolumeclaims,verbs=watch;get;create;update;list;delete 38 39 func (r *ClusterPeerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { 40 defer shared.IgnoreConflicts(&err) 41 42 var peer ipfsv1alpha1.ClusterPeer 43 44 if err = r.Client.Get(ctx, req.NamespacedName, &peer); err != nil { 45 err = client.IgnoreNotFound(err) 46 return 47 } 48 49 // default the cluster peer if webhooks are disabled 50 if !shared.IsWebhookEnabled() { 51 peer.Default() 52 } 53 54 shared.UpdateLabels(&peer, "ipfs-cluster-service", "") 55 56 // reconcile service 57 if err = r.ReconcileOwned(ctx, &peer, &corev1.Service{}, func(obj client.Object) error { 58 r.specService(&peer, obj.(*corev1.Service)) 59 return nil 60 }); err != nil { 61 return 62 } 63 64 // reconcile persistent volume claim 65 if err = r.ReconcileOwned(ctx, &peer, &corev1.PersistentVolumeClaim{}, func(obj client.Object) error { 66 r.specPVC(&peer, obj.(*corev1.PersistentVolumeClaim)) 67 return nil 68 }); err != nil { 69 return 70 } 71 72 // reconcile config map 73 if err = r.ReconcileOwned(ctx, &peer, &corev1.ConfigMap{}, func(obj client.Object) error { 74 r.specConfigmap(&peer, obj.(*corev1.ConfigMap)) 75 return nil 76 }); err != nil { 77 return 78 } 79 80 // reconcile stateful set 81 if err = r.ReconcileOwned(ctx, &peer, &appsv1.StatefulSet{}, func(obj client.Object) error { 82 client, err := ipfsClients.NewClient(&peer) 83 if err != nil { 84 return err 85 } 86 87 command := client.Command() 88 args := client.Args() 89 args = append(args, peer.Spec.ExtraArgs.Encode(false)...) 90 env := client.Env() 91 homeDir := client.HomeDir() 92 93 r.specStatefulset(&peer, obj.(*appsv1.StatefulSet), homeDir, env, command, args) 94 return nil 95 }); err != nil { 96 return 97 } 98 99 if err = r.updateStatus(ctx, &peer); err != nil { 100 return 101 } 102 103 return 104 } 105 106 // updateStatus updates ipfs cluster peer status 107 func (r *ClusterPeerReconciler) updateStatus(ctx context.Context, peer *ipfsv1alpha1.ClusterPeer) error { 108 // TODO: update after multi-client support 109 peer.Status.Client = "ipfs-cluster-service" 110 111 if err := r.Status().Update(ctx, peer); err != nil { 112 log.FromContext(ctx).Error(err, "unable to update cluster peer status") 113 return err 114 } 115 116 return nil 117 } 118 119 // specService updates ipfs peer service spec 120 func (r *ClusterPeerReconciler) specService(peer *ipfsv1alpha1.ClusterPeer, svc *corev1.Service) { 121 labels := peer.Labels 122 123 svc.ObjectMeta.Labels = labels 124 125 svc.Spec.Ports = []corev1.ServicePort{ 126 { 127 Name: "swarm", 128 Port: 9096, 129 TargetPort: intstr.FromString("swarm"), 130 }, 131 { 132 Name: "swarm-udp", 133 Port: 9096, 134 TargetPort: intstr.FromString("swarm-udp"), 135 Protocol: corev1.ProtocolUDP, 136 }, 137 { 138 // Pinning service API 139 // https://ipfscluster.io/documentation/reference/pinsvc_api/ 140 Name: "api", 141 Port: 5001, 142 TargetPort: intstr.FromString("api"), 143 }, 144 { 145 // Proxy API 146 // https://ipfscluster.io/documentation/reference/proxy/ 147 Name: "proxy-api", 148 Port: 9095, 149 TargetPort: intstr.FromString("proxy-api"), 150 }, 151 { 152 // REST API 153 //https://ipfscluster.io/documentation/reference/api/ 154 Name: "rest", 155 Port: 9094, 156 TargetPort: intstr.FromString("rest"), 157 }, 158 { 159 Name: "metrics", 160 Port: 8888, 161 TargetPort: intstr.FromString("metrics"), 162 }, 163 { 164 Name: "tracing", 165 Port: 6831, 166 TargetPort: intstr.FromString("tracing"), 167 }, 168 } 169 170 svc.Spec.Selector = labels 171 } 172 173 // specConfigmap updates IPFS cluster peer configmap spec 174 func (r *ClusterPeerReconciler) specConfigmap(peer *ipfsv1alpha1.ClusterPeer, config *corev1.ConfigMap) { 175 config.ObjectMeta.Labels = peer.Labels 176 177 if config.Data == nil { 178 config.Data = make(map[string]string) 179 } 180 181 config.Data["init_ipfs_cluster_config.sh"] = initIPFSClusterConfig 182 } 183 184 // specPVC updates IPFS cluster peer persistent volume claim 185 func (r *ClusterPeerReconciler) specPVC(peer *ipfsv1alpha1.ClusterPeer, pvc *corev1.PersistentVolumeClaim) { 186 request := corev1.ResourceList{ 187 corev1.ResourceStorage: resource.MustParse(peer.Spec.Resources.Storage), 188 } 189 190 // spec is immutable after creation except resources.requests for bound claims 191 if !pvc.CreationTimestamp.IsZero() { 192 pvc.Spec.Resources.Requests = request 193 return 194 } 195 196 pvc.ObjectMeta.Labels = peer.Labels 197 pvc.Spec = corev1.PersistentVolumeClaimSpec{ 198 AccessModes: []corev1.PersistentVolumeAccessMode{ 199 corev1.ReadWriteOnce, 200 }, 201 Resources: corev1.VolumeResourceRequirements{ 202 Requests: request, 203 }, 204 StorageClassName: peer.Spec.Resources.StorageClass, 205 } 206 } 207 208 // specStatefulset updates IPFS cluster peer statefulset 209 func (r *ClusterPeerReconciler) specStatefulset(peer *ipfsv1alpha1.ClusterPeer, sts *appsv1.StatefulSet, homeDir string, env []corev1.EnvVar, command, args []string) { 210 labels := peer.Labels 211 212 sts.Labels = labels 213 214 // environment variables required by `ipfs-cluster-service init` 215 initClusterPeerENV := []corev1.EnvVar{ 216 { 217 Name: ipfsClients.EnvIPFSClusterPath, 218 Value: shared.PathData(homeDir), 219 }, 220 { 221 Name: ipfsClients.EnvIPFSClusterConsensus, 222 Value: string(peer.Spec.Consensus), 223 }, 224 { 225 Name: ipfsClients.EnvIPFSClusterPeerEndpoint, 226 Value: peer.Spec.PeerEndpoint, 227 }, 228 { 229 Name: ipfsClients.EnvIPFSClusterSecret, 230 ValueFrom: &corev1.EnvVarSource{ 231 SecretKeyRef: &corev1.SecretKeySelector{ 232 LocalObjectReference: corev1.LocalObjectReference{ 233 Name: peer.Spec.ClusterSecretName, 234 }, 235 Key: "secret", 236 }, 237 }, 238 }, 239 { 240 Name: ipfsClients.EnvIPFSClusterTrustedPeers, 241 Value: strings.Join(peer.Spec.TrustedPeers, ","), 242 }, 243 } 244 245 // if cluster peer ID (which implies private key) is provided 246 // append cluster id and private key environment variables 247 if peer.Spec.ID != "" { 248 // cluster id 249 initClusterPeerENV = append(initClusterPeerENV, corev1.EnvVar{ 250 Name: ipfsClients.EnvIPFSClusterId, 251 Value: peer.Spec.ID, 252 }) 253 // cluster private key 254 initClusterPeerENV = append(initClusterPeerENV, corev1.EnvVar{ 255 Name: ipfsClients.EnvIPFSClusterPrivateKey, 256 ValueFrom: &corev1.EnvVarSource{ 257 SecretKeyRef: &corev1.SecretKeySelector{ 258 LocalObjectReference: corev1.LocalObjectReference{ 259 Name: peer.Spec.PrivateKeySecretName, 260 }, 261 Key: "key", 262 }, 263 }, 264 }) 265 } 266 267 ports := []corev1.ContainerPort{ 268 { 269 Name: "swarm", 270 ContainerPort: 9096, 271 }, 272 { 273 Name: "swarm-udp", 274 ContainerPort: 9096, 275 Protocol: corev1.ProtocolUDP, 276 }, 277 { 278 Name: "api", 279 ContainerPort: 5001, 280 }, 281 { 282 Name: "proxy-api", 283 ContainerPort: 9095, 284 }, 285 { 286 Name: "rest", 287 ContainerPort: 9094, 288 }, 289 { 290 Name: "metrics", 291 ContainerPort: 8888, 292 }, 293 { 294 Name: "tracing", 295 ContainerPort: 6831, 296 }, 297 } 298 299 replicas := int32(*peer.Spec.Replicas) 300 301 sts.Spec = appsv1.StatefulSetSpec{ 302 Selector: &metav1.LabelSelector{ 303 MatchLabels: labels, 304 }, 305 Replicas: &replicas, 306 Template: corev1.PodTemplateSpec{ 307 ObjectMeta: metav1.ObjectMeta{ 308 Labels: labels, 309 }, 310 Spec: corev1.PodSpec{ 311 SecurityContext: shared.SecurityContext(), 312 InitContainers: []corev1.Container{ 313 { 314 Name: "init-cluster-peer", 315 Image: peer.Spec.Image, 316 Command: []string{"/bin/sh"}, 317 Env: initClusterPeerENV, 318 Args: []string{ 319 fmt.Sprintf("%s/init_ipfs_cluster_config.sh", shared.PathConfig(homeDir)), 320 }, 321 VolumeMounts: []corev1.VolumeMount{ 322 { 323 Name: "data", 324 MountPath: shared.PathData(homeDir), 325 }, 326 { 327 Name: "config", 328 MountPath: shared.PathConfig(homeDir), 329 }, 330 }, 331 }, 332 }, 333 Containers: []corev1.Container{ 334 { 335 Name: "cluster-peer", 336 Image: peer.Spec.Image, 337 Command: command, 338 Env: env, 339 Args: args, 340 Ports: ports, 341 VolumeMounts: []corev1.VolumeMount{ 342 { 343 Name: "data", 344 MountPath: shared.PathData(homeDir), 345 }, 346 }, 347 Resources: corev1.ResourceRequirements{ 348 Requests: corev1.ResourceList{ 349 corev1.ResourceCPU: resource.MustParse(peer.Spec.CPU), 350 corev1.ResourceMemory: resource.MustParse(peer.Spec.Memory), 351 }, 352 Limits: corev1.ResourceList{ 353 corev1.ResourceCPU: resource.MustParse(peer.Spec.CPULimit), 354 corev1.ResourceMemory: resource.MustParse(peer.Spec.MemoryLimit), 355 }, 356 }, 357 }, 358 }, 359 Volumes: []corev1.Volume{ 360 { 361 Name: "data", 362 VolumeSource: corev1.VolumeSource{ 363 PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ 364 ClaimName: peer.Name, 365 }, 366 }, 367 }, 368 { 369 Name: "config", 370 VolumeSource: corev1.VolumeSource{ 371 ConfigMap: &corev1.ConfigMapVolumeSource{ 372 LocalObjectReference: corev1.LocalObjectReference{ 373 Name: peer.Name, 374 }, 375 }, 376 }, 377 }, 378 }, 379 }, 380 }, 381 } 382 } 383 384 func (r *ClusterPeerReconciler) SetupWithManager(mgr ctrl.Manager) error { 385 pred := predicate.GenerationChangedPredicate{} 386 return ctrl.NewControllerManagedBy(mgr). 387 For(&ipfsv1alpha1.ClusterPeer{}). 388 WithEventFilter(pred). 389 Owns(&appsv1.StatefulSet{}). 390 Owns(&corev1.PersistentVolumeClaim{}). 391 Owns(&corev1.Service{}). 392 Owns(&corev1.ConfigMap{}). 393 Complete(r) 394 }