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 }