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  }