github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/caas/kubernetes/provider/statefulsets.go (about)

     1  // Copyright 2020 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package provider
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  
    10  	"github.com/juju/errors"
    11  	apps "k8s.io/api/apps/v1"
    12  	core "k8s.io/api/core/v1"
    13  	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    14  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    15  	"k8s.io/utils/pointer"
    16  
    17  	"github.com/juju/juju/caas/kubernetes/provider/constants"
    18  	k8sstorage "github.com/juju/juju/caas/kubernetes/provider/storage"
    19  	"github.com/juju/juju/caas/kubernetes/provider/utils"
    20  	"github.com/juju/juju/caas/specs"
    21  	k8sannotations "github.com/juju/juju/core/annotations"
    22  	"github.com/juju/juju/storage"
    23  )
    24  
    25  // https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#update-strategies
    26  func updateStrategyForStatefulSet(strategy specs.UpdateStrategy) (o apps.StatefulSetUpdateStrategy, err error) {
    27  	strategyType := apps.StatefulSetUpdateStrategyType(strategy.Type)
    28  
    29  	o = apps.StatefulSetUpdateStrategy{Type: strategyType}
    30  	switch strategyType {
    31  	case apps.OnDeleteStatefulSetStrategyType:
    32  		if strategy.RollingUpdate != nil {
    33  			return o, errors.NewNotValid(nil, fmt.Sprintf("rolling update spec is not supported for %q", strategyType))
    34  		}
    35  	case apps.RollingUpdateStatefulSetStrategyType:
    36  		if strategy.RollingUpdate != nil {
    37  			if strategy.RollingUpdate.MaxSurge != nil || strategy.RollingUpdate.MaxUnavailable != nil {
    38  				return o, errors.NotValidf("rolling update spec for statefulset")
    39  			}
    40  			if strategy.RollingUpdate.Partition == nil {
    41  				return o, errors.New("rolling update spec partition is missing")
    42  			}
    43  			o.RollingUpdate = &apps.RollingUpdateStatefulSetStrategy{
    44  				Partition: strategy.RollingUpdate.Partition,
    45  			}
    46  		}
    47  	default:
    48  		return o, errors.NotValidf("strategy type %q for statefulset", strategyType)
    49  	}
    50  	return o, nil
    51  }
    52  
    53  func (k *kubernetesClient) configureStatefulSet(
    54  	appName, deploymentName string, annotations k8sannotations.Annotation, workloadSpec *workloadSpec,
    55  	containers []specs.ContainerSpec, replicas *int32, filesystems []storage.KubernetesFilesystemParams,
    56  ) error {
    57  	logger.Debugf("creating/updating stateful set for %s", appName)
    58  
    59  	// Add the specified file to the pod spec.
    60  	cfgName := func(fileSetName string) string {
    61  		return applicationConfigMapName(deploymentName, fileSetName)
    62  	}
    63  
    64  	storageUniqueID, err := k.getStorageUniqPrefix(func() (annotationGetter, error) {
    65  		return k.getStatefulSet(deploymentName)
    66  	})
    67  	if err != nil {
    68  		return errors.Trace(err)
    69  	}
    70  
    71  	selectorLabels := utils.SelectorLabelsForApp(appName, k.IsLegacyLabels())
    72  	statefulSet := &apps.StatefulSet{
    73  		ObjectMeta: v1.ObjectMeta{
    74  			Name:   deploymentName,
    75  			Labels: utils.LabelsForApp(appName, k.IsLegacyLabels()),
    76  			Annotations: k8sannotations.New(nil).
    77  				Merge(annotations).
    78  				Add(utils.AnnotationKeyApplicationUUID(k.IsLegacyLabels()), storageUniqueID).ToMap(),
    79  		},
    80  		Spec: apps.StatefulSetSpec{
    81  			Replicas: replicas,
    82  			Selector: &v1.LabelSelector{
    83  				MatchLabels: selectorLabels,
    84  			},
    85  			RevisionHistoryLimit: pointer.Int32Ptr(statefulSetRevisionHistoryLimit),
    86  			Template: core.PodTemplateSpec{
    87  				ObjectMeta: v1.ObjectMeta{
    88  					Labels:      utils.LabelsMerge(workloadSpec.Pod.Labels, selectorLabels),
    89  					Annotations: podAnnotations(k8sannotations.New(workloadSpec.Pod.Annotations).Merge(annotations).Copy()).ToMap(),
    90  				},
    91  			},
    92  			PodManagementPolicy: getPodManagementPolicy(workloadSpec.Service),
    93  			ServiceName:         headlessServiceName(deploymentName),
    94  		},
    95  	}
    96  	if workloadSpec.Service != nil && workloadSpec.Service.UpdateStrategy != nil {
    97  		if statefulSet.Spec.UpdateStrategy, err = updateStrategyForStatefulSet(*workloadSpec.Service.UpdateStrategy); err != nil {
    98  			return errors.Trace(err)
    99  		}
   100  	}
   101  
   102  	if err := k.configurePodFiles(appName, annotations, workloadSpec, containers, cfgName); err != nil {
   103  		return errors.Trace(err)
   104  	}
   105  	podSpec := workloadSpec.Pod.PodSpec
   106  	existingPodSpec := podSpec
   107  
   108  	handlePVC := func(pvc core.PersistentVolumeClaim, mountPath string, readOnly bool) error {
   109  		if readOnly {
   110  			logger.Warningf("set storage mode to ReadOnlyMany if read only storage is needed")
   111  		}
   112  		if err := k8sstorage.PushUniqueVolumeClaimTemplate(&statefulSet.Spec, pvc); err != nil {
   113  			return errors.Trace(err)
   114  		}
   115  		podSpec.Containers[0].VolumeMounts = append(podSpec.Containers[0].VolumeMounts, core.VolumeMount{
   116  			Name:      pvc.Name,
   117  			MountPath: mountPath,
   118  		})
   119  		return nil
   120  	}
   121  	if err = k.configureStorage(appName, isLegacyName(deploymentName), storageUniqueID, filesystems, &podSpec, handlePVC); err != nil {
   122  		return errors.Trace(err)
   123  	}
   124  	statefulSet.Spec.Template.Spec = podSpec
   125  	return k.ensureStatefulSet(statefulSet, existingPodSpec)
   126  }
   127  
   128  func (k *kubernetesClient) ensureStatefulSet(spec *apps.StatefulSet, existingPodSpec core.PodSpec) error {
   129  	_, err := k.createStatefulSet(spec)
   130  	if errors.IsNotValid(err) {
   131  		return errors.Annotatef(err, "ensuring stateful set %q", spec.GetName())
   132  	} else if errors.IsAlreadyExists(err) {
   133  		// continue
   134  	} else if err != nil {
   135  		return errors.Trace(err)
   136  	} else {
   137  		return nil
   138  	}
   139  	// The statefulset already exists so all we are allowed to update is replicas,
   140  	// template, update strategy. Juju may hand out info with a slightly different
   141  	// requested volume size due to trying to adapt the unit model to the k8s world.
   142  	existing, err := k.getStatefulSet(spec.GetName())
   143  	if err != nil {
   144  		return errors.Trace(err)
   145  	}
   146  	existing.SetAnnotations(spec.GetAnnotations())
   147  	existing.Spec.Replicas = spec.Spec.Replicas
   148  	existing.Spec.UpdateStrategy = spec.Spec.UpdateStrategy
   149  	existing.Spec.Template.Spec.Volumes = existingPodSpec.Volumes
   150  	existing.Spec.Template.SetAnnotations(spec.Spec.Template.GetAnnotations())
   151  	// TODO(caas) - allow storage `request` configurable - currently we only allow `limit`.
   152  	existing.Spec.Template.Spec.InitContainers = existingPodSpec.InitContainers
   153  	existing.Spec.Template.Spec.Containers = existingPodSpec.Containers
   154  	existing.Spec.Template.Spec.ServiceAccountName = existingPodSpec.ServiceAccountName
   155  	existing.Spec.Template.Spec.AutomountServiceAccountToken = existingPodSpec.AutomountServiceAccountToken
   156  	// NB: we can't update the Spec.ServiceName as it is immutable.
   157  	_, err = k.updateStatefulSet(existing)
   158  	return errors.Trace(err)
   159  }
   160  
   161  func (k *kubernetesClient) createStatefulSet(spec *apps.StatefulSet) (*apps.StatefulSet, error) {
   162  	if k.namespace == "" {
   163  		return nil, errNoNamespace
   164  	}
   165  	out, err := k.client().AppsV1().StatefulSets(k.namespace).Create(context.TODO(), spec, v1.CreateOptions{})
   166  	if k8serrors.IsAlreadyExists(err) {
   167  		return nil, errors.AlreadyExistsf("stateful set %q", spec.GetName())
   168  	}
   169  	if k8serrors.IsInvalid(err) {
   170  		return nil, errors.NotValidf("stateful set %q", spec.GetName())
   171  	}
   172  	return out, errors.Trace(err)
   173  }
   174  
   175  func (k *kubernetesClient) updateStatefulSet(spec *apps.StatefulSet) (*apps.StatefulSet, error) {
   176  	if k.namespace == "" {
   177  		return nil, errNoNamespace
   178  	}
   179  	out, err := k.client().AppsV1().StatefulSets(k.namespace).Update(context.TODO(), spec, v1.UpdateOptions{})
   180  	if k8serrors.IsNotFound(err) {
   181  		return nil, errors.NotFoundf("stateful set %q", spec.GetName())
   182  	}
   183  	if k8serrors.IsInvalid(err) {
   184  		return nil, errors.NotValidf("stateful set %q", spec.GetName())
   185  	}
   186  	return out, errors.Trace(err)
   187  }
   188  
   189  func (k *kubernetesClient) getStatefulSet(name string) (*apps.StatefulSet, error) {
   190  	if k.namespace == "" {
   191  		return nil, errNoNamespace
   192  	}
   193  	out, err := k.client().AppsV1().StatefulSets(k.namespace).Get(context.TODO(), name, v1.GetOptions{})
   194  	if k8serrors.IsNotFound(err) {
   195  		return nil, errors.NotFoundf("stateful set %q", name)
   196  	}
   197  	return out, errors.Trace(err)
   198  }
   199  
   200  // deleteStatefulSet deletes a statefulset resource.
   201  func (k *kubernetesClient) deleteStatefulSet(name string) error {
   202  	if k.namespace == "" {
   203  		return errNoNamespace
   204  	}
   205  	err := k.client().AppsV1().StatefulSets(k.namespace).Delete(context.TODO(), name, v1.DeleteOptions{
   206  		PropagationPolicy: constants.DefaultPropagationPolicy(),
   207  	})
   208  	if k8serrors.IsNotFound(err) {
   209  		return nil
   210  	}
   211  	return errors.Trace(err)
   212  }
   213  
   214  // deleteStatefulSet deletes all statefulset resources for an application.
   215  func (k *kubernetesClient) deleteStatefulSets(appName string) error {
   216  	if k.namespace == "" {
   217  		return errNoNamespace
   218  	}
   219  	labels := utils.LabelsForApp(appName, k.IsLegacyLabels())
   220  	err := k.client().AppsV1().StatefulSets(k.namespace).DeleteCollection(context.TODO(), v1.DeleteOptions{
   221  		PropagationPolicy: constants.DefaultPropagationPolicy(),
   222  	}, v1.ListOptions{
   223  		LabelSelector: utils.LabelsToSelector(labels).String(),
   224  	})
   225  	if k8serrors.IsNotFound(err) {
   226  		return nil
   227  	}
   228  	return errors.Trace(err)
   229  }