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  }