|
16 | 16 |
|
17 | 17 | package thin |
18 | 18 |
|
19 | | -import cruntime "github.com/fluid-cloudnative/fluid/pkg/runtime" |
| 19 | +import ( |
| 20 | + "context" |
| 21 | + "reflect" |
| 22 | + |
| 23 | + appsv1 "k8s.io/api/apps/v1" |
| 24 | + corev1 "k8s.io/api/core/v1" |
| 25 | + apierrs "k8s.io/apimachinery/pkg/api/errors" |
| 26 | + "k8s.io/apimachinery/pkg/types" |
| 27 | + "k8s.io/client-go/util/retry" |
| 28 | + |
| 29 | + datav1alpha1 "github.com/fluid-cloudnative/fluid/api/v1alpha1" |
| 30 | + "github.com/fluid-cloudnative/fluid/pkg/common" |
| 31 | + cruntime "github.com/fluid-cloudnative/fluid/pkg/runtime" |
| 32 | + "github.com/fluid-cloudnative/fluid/pkg/utils" |
| 33 | + "github.com/fluid-cloudnative/fluid/pkg/utils/kubeclient" |
| 34 | + runtimeOpts "github.com/fluid-cloudnative/fluid/pkg/utils/runtimes/options" |
| 35 | +) |
| 36 | + |
| 37 | +// fuseContainerName is the name of the fuse container rendered by the thin chart, |
| 38 | +// see charts/thin/templates/fuse/daemonset.yaml. |
| 39 | +const fuseContainerName = "thin-fuse" |
| 40 | + |
| 41 | +// SyncRuntime reconciles the fuse template fields of a ThinRuntime that has already been set up. |
| 42 | +// Without it, editing a ready ThinRuntime's spec.fuse never reaches the rendered values or the fuse |
| 43 | +// DaemonSet, because they are only ever generated once during setup. |
| 44 | +// |
| 45 | +// The helm values ConfigMap is treated as the last synced state: the desired state is re-rendered |
| 46 | +// from the ThinRuntime and its ThinRuntimeProfile, diffed against that last synced state, pushed to |
| 47 | +// the DaemonSet, and only then committed back to the ConfigMap. Doing it in that order means an |
| 48 | +// interrupted sync is retried on the next reconciliation instead of being silently forgotten. |
| 49 | +// |
| 50 | +// The fuse DaemonSet uses the OnDelete update strategy, so updating its template does not restart |
| 51 | +// running fuse pods. They pick up the new template whenever they are deleted, which is why the |
| 52 | +// change is also surfaced as an event. |
| 53 | +func (t *ThinEngine) SyncRuntime(ctx cruntime.ReconcileRequestContext) (changed bool, err error) { |
| 54 | + if runtimeOpts.ShouldSkipSyncingRuntime() { |
| 55 | + t.Log.V(1).Info("Skipping runtime sync due to CONTROLLER_SKIP_SYNCING_RUNTIME being enabled") |
| 56 | + return |
| 57 | + } |
| 58 | + |
| 59 | + runtime, err := t.getRuntime() |
| 60 | + if err != nil { |
| 61 | + return |
| 62 | + } |
| 63 | + |
| 64 | + // The engine is cached across reconciliations, so t.runtime and t.runtimeProfile may be stale. |
| 65 | + profile, err := utils.GetThinRuntimeProfile(t.Client, runtime.Spec.ThinRuntimeProfileName) |
| 66 | + if err != nil { |
| 67 | + if apierrs.IsNotFound(err) { |
| 68 | + t.Log.Info("ThinRuntimeProfile not found, skip syncing the runtime spec", |
| 69 | + "profile", runtime.Spec.ThinRuntimeProfileName) |
| 70 | + return false, nil |
| 71 | + } |
| 72 | + return false, err |
| 73 | + } |
| 74 | + |
| 75 | + latestValue, err := t.transform(runtime, profile) |
| 76 | + if err != nil { |
| 77 | + return |
| 78 | + } |
| 79 | + |
| 80 | + err = retry.RetryOnConflict(retry.DefaultBackoff, func() error { |
| 81 | + valueToSync, innerErr := t.GetValueFromConfigmap() |
| 82 | + if innerErr != nil { |
| 83 | + return innerErr |
| 84 | + } |
| 85 | + if valueToSync == nil { |
| 86 | + // The user opted out of the values ConfigMap, so there is no last synced state to diff |
| 87 | + // against. Degrade to not syncing rather than failing the whole reconciliation. |
| 88 | + t.Log.Info("Helm value configmap not found, skip syncing the runtime spec", |
| 89 | + "configmap", t.getHelmValuesConfigMapName()) |
| 90 | + return nil |
| 91 | + } |
| 92 | + |
| 93 | + fuseChanged, innerErr := t.syncFuseSpec(valueToSync, latestValue) |
| 94 | + if innerErr != nil { |
| 95 | + return innerErr |
| 96 | + } |
| 97 | + |
| 98 | + changed = fuseChanged |
| 99 | + if !changed { |
| 100 | + return nil |
| 101 | + } |
| 102 | + |
| 103 | + t.Log.Info("Committing the changed value to the helm value configmap", "name", t.name, "namespace", t.namespace) |
| 104 | + if innerErr = t.SaveValueToConfigmap(valueToSync); innerErr != nil { |
| 105 | + t.Log.Error(innerErr, "failed to save the changed value to the helm value configmap") |
| 106 | + return innerErr |
| 107 | + } |
| 108 | + |
| 109 | + return nil |
| 110 | + }) |
| 111 | + |
| 112 | + if err != nil { |
| 113 | + t.Log.Error(err, "Failed to sync the runtime spec") |
| 114 | + return false, err |
| 115 | + } |
| 116 | + |
| 117 | + if changed { |
| 118 | + t.recordFuseTemplateUpdated(runtime) |
| 119 | + } |
20 | 120 |
|
21 | | -func (t ThinEngine) SyncRuntime(ctx cruntime.ReconcileRequestContext) (changed bool, err error) { |
22 | 121 | return |
23 | 122 | } |
| 123 | + |
| 124 | +func (t *ThinEngine) recordFuseTemplateUpdated(runtime *datav1alpha1.ThinRuntime) { |
| 125 | + if t.Recorder == nil { |
| 126 | + return |
| 127 | + } |
| 128 | + t.Recorder.Eventf(runtime, corev1.EventTypeNormal, common.FuseTemplateUpdated, |
| 129 | + "Updated the template of fuse daemonset %s. Its update strategy is OnDelete, so running fuse pods keep the previous template until they are deleted.", |
| 130 | + t.getFuseName()) |
| 131 | +} |
| 132 | + |
| 133 | +// syncFuseSpec pushes the differences between the last synced value and the latest value into the |
| 134 | +// fuse DaemonSet. oldValue is advanced in place for every field that was actually pushed, so that |
| 135 | +// the caller can commit it as the new last synced state. |
| 136 | +func (t *ThinEngine) syncFuseSpec(oldValue, latestValue *ThinValue) (changed bool, err error) { |
| 137 | + t.Log.V(1).Info("entering syncFuseSpec") |
| 138 | + defer func() { |
| 139 | + t.Log.V(1).Info("exiting syncFuseSpec") |
| 140 | + }() |
| 141 | + |
| 142 | + fuses, err := kubeclient.GetDaemonset(t.Client, t.getFuseName(), t.namespace) |
| 143 | + if err != nil { |
| 144 | + return false, err |
| 145 | + } |
| 146 | + |
| 147 | + if fuses.Spec.UpdateStrategy.Type != appsv1.OnDeleteDaemonSetStrategyType { |
| 148 | + // Updating the template of a RollingUpdate daemonset would restart the running fuse pods and |
| 149 | + // break the applications mounting them, so switch the strategy first and let the resulting |
| 150 | + // update event trigger a new reconciliation. |
| 151 | + t.Log.V(1).Info("Fuse daemonset's update strategy is not safe to sync fuse spec", |
| 152 | + "updateStrategy", fuses.Spec.UpdateStrategy.Type) |
| 153 | + if err = kubeclient.UpdateDaemonSetUpdateStrategy(t.Client, fuses.Name, fuses.Namespace, |
| 154 | + appsv1.DaemonSetUpdateStrategy{Type: appsv1.OnDeleteDaemonSetStrategyType}); err != nil { |
| 155 | + return false, err |
| 156 | + } |
| 157 | + t.Log.Info("syncFuseSpec: successfully updated fuse daemonset's update strategy to OnDelete", |
| 158 | + "fuse ds", types.NamespacedName{Namespace: fuses.Namespace, Name: fuses.Name}) |
| 159 | + return false, nil |
| 160 | + } |
| 161 | + |
| 162 | + fusesToUpdate := fuses.DeepCopy() |
| 163 | + changed, err = t.checkAndSetFuseChanges(oldValue, latestValue, fusesToUpdate) |
| 164 | + if err != nil { |
| 165 | + return false, err |
| 166 | + } |
| 167 | + if !changed { |
| 168 | + t.Log.V(1).Info("syncFuseSpec: no differences detected about fuse") |
| 169 | + return false, nil |
| 170 | + } |
| 171 | + |
| 172 | + if reflect.DeepEqual(fuses, fusesToUpdate) { |
| 173 | + t.Log.V(1).Info("syncFuseSpec: no differences detected about fuse after equality check") |
| 174 | + return false, nil |
| 175 | + } |
| 176 | + |
| 177 | + t.Log.Info("syncFuseSpec: some fields are changed in fuse, try to update the fuse daemonset", |
| 178 | + "fuse ds", types.NamespacedName{Namespace: fusesToUpdate.Namespace, Name: fusesToUpdate.Name}) |
| 179 | + if err = t.Client.Update(context.TODO(), fusesToUpdate); err != nil { |
| 180 | + t.Log.Error(err, "syncFuseSpec: failed to update the fuse daemonset spec", |
| 181 | + "fuse ds", types.NamespacedName{Namespace: fusesToUpdate.Namespace, Name: fusesToUpdate.Name}) |
| 182 | + return false, err |
| 183 | + } |
| 184 | + |
| 185 | + return true, nil |
| 186 | +} |
| 187 | + |
| 188 | +// checkAndSetFuseChanges applies the supported fuse template changes onto fusesToUpdate. |
| 189 | +// |
| 190 | +// Fields the thin chart renders verbatim (resources, image, imagePullPolicy) are compared against |
| 191 | +// the live daemonset, so that manual drift is corrected as well. Fields the chart merges with |
| 192 | +// entries of its own (envs, volumes, volumeMounts, labels, annotations) are compared against the |
| 193 | +// last synced value instead, and only the value-derived entries are replaced, so that the chart's |
| 194 | +// own entries survive. |
| 195 | +// |
| 196 | +// Some fuse fields are deliberately left out: |
| 197 | +// - nodeSelector, because transformFuse injects the fuse scheduling label that CSI relies on to |
| 198 | +// place fuse pods, so changing it after creation breaks mounting. |
| 199 | +// - hostNetwork, hostPID, targetPath, ports, command, args and the probes, whose post-ready change |
| 200 | +// semantics need their own discussion. |
| 201 | +// - configValue, which updateFuseConfigOnChange already reconciles. |
| 202 | +func (t *ThinEngine) checkAndSetFuseChanges(oldValue, latestValue *ThinValue, fusesToUpdate *appsv1.DaemonSet) (changed bool, err error) { |
| 203 | + // volumes |
| 204 | + if !isSliceEqual(oldValue.Fuse.Volumes, latestValue.Fuse.Volumes) { |
| 205 | + t.Log.Info("syncFuseSpec: volumes changed", "old", oldValue.Fuse.Volumes, "new", latestValue.Fuse.Volumes) |
| 206 | + fusesToUpdate.Spec.Template.Spec.Volumes = append( |
| 207 | + utils.GetVolumesDifference(fusesToUpdate.Spec.Template.Spec.Volumes, oldValue.Fuse.Volumes), |
| 208 | + latestValue.Fuse.Volumes...) |
| 209 | + oldValue.Fuse.Volumes = latestValue.Fuse.Volumes |
| 210 | + changed = true |
| 211 | + } |
| 212 | + |
| 213 | + // labels |
| 214 | + if !isMapEqual(oldValue.Fuse.Labels, latestValue.Fuse.Labels) { |
| 215 | + t.Log.Info("syncFuseSpec: labels changed", "old", oldValue.Fuse.Labels, "new", latestValue.Fuse.Labels) |
| 216 | + fusesToUpdate.Spec.Template.Labels = utils.UnionMapsWithOverride( |
| 217 | + utils.GetMapsDifference(fusesToUpdate.Spec.Template.Labels, oldValue.Fuse.Labels), |
| 218 | + latestValue.Fuse.Labels) |
| 219 | + oldValue.Fuse.Labels = latestValue.Fuse.Labels |
| 220 | + changed = true |
| 221 | + } |
| 222 | + |
| 223 | + // annotations |
| 224 | + if !isMapEqual(oldValue.Fuse.Annotations, latestValue.Fuse.Annotations) { |
| 225 | + t.Log.Info("syncFuseSpec: annotations changed", "old", oldValue.Fuse.Annotations, "new", latestValue.Fuse.Annotations) |
| 226 | + fusesToUpdate.Spec.Template.Annotations = utils.UnionMapsWithOverride( |
| 227 | + utils.GetMapsDifference(fusesToUpdate.Spec.Template.Annotations, oldValue.Fuse.Annotations), |
| 228 | + latestValue.Fuse.Annotations) |
| 229 | + oldValue.Fuse.Annotations = latestValue.Fuse.Annotations |
| 230 | + changed = true |
| 231 | + } |
| 232 | + |
| 233 | + containerIdx := utils.GetContainerIndex(fusesToUpdate.Spec.Template.Spec.Containers, fuseContainerName) |
| 234 | + if containerIdx < 0 { |
| 235 | + t.Log.Info("syncFuseSpec: fuse container not found in the fuse daemonset, skip syncing the container spec", |
| 236 | + "container", fuseContainerName) |
| 237 | + return changed, nil |
| 238 | + } |
| 239 | + container := &fusesToUpdate.Spec.Template.Spec.Containers[containerIdx] |
| 240 | + |
| 241 | + // resources |
| 242 | + latestResources, err := utils.TransformInternalResourcesToCoreV1Resources(latestValue.Fuse.Resources) |
| 243 | + if err != nil { |
| 244 | + return false, err |
| 245 | + } |
| 246 | + if !utils.ResourceRequirementsEqual(container.Resources, latestResources) { |
| 247 | + t.Log.Info("syncFuseSpec: resources changed", "old", container.Resources, "new", latestResources) |
| 248 | + container.Resources = latestResources |
| 249 | + oldValue.Fuse.Resources = latestValue.Fuse.Resources |
| 250 | + changed = true |
| 251 | + } |
| 252 | + |
| 253 | + // image |
| 254 | + if latestImage := composeImage(latestValue.Fuse.Image, latestValue.Fuse.ImageTag); container.Image != latestImage { |
| 255 | + t.Log.Info("syncFuseSpec: image changed", "old", container.Image, "new", latestImage) |
| 256 | + container.Image = latestImage |
| 257 | + oldValue.Fuse.Image = latestValue.Fuse.Image |
| 258 | + oldValue.Fuse.ImageTag = latestValue.Fuse.ImageTag |
| 259 | + changed = true |
| 260 | + } |
| 261 | + |
| 262 | + // imagePullPolicy |
| 263 | + // An empty value means the chart falls back to the Kubernetes default, so leave the daemonset |
| 264 | + // alone instead of clearing a policy that is already in effect. |
| 265 | + if latestPullPolicy := corev1.PullPolicy(latestValue.Fuse.ImagePullPolicy); latestPullPolicy != "" && |
| 266 | + container.ImagePullPolicy != latestPullPolicy { |
| 267 | + t.Log.Info("syncFuseSpec: image pull policy changed", "old", container.ImagePullPolicy, "new", latestPullPolicy) |
| 268 | + container.ImagePullPolicy = latestPullPolicy |
| 269 | + oldValue.Fuse.ImagePullPolicy = latestValue.Fuse.ImagePullPolicy |
| 270 | + changed = true |
| 271 | + } |
| 272 | + |
| 273 | + // envs |
| 274 | + if !isSliceEqual(oldValue.Fuse.Envs, latestValue.Fuse.Envs) { |
| 275 | + t.Log.Info("syncFuseSpec: env variables changed", "old", oldValue.Fuse.Envs, "new", latestValue.Fuse.Envs) |
| 276 | + container.Env = append( |
| 277 | + utils.GetEnvsDifference(container.Env, oldValue.Fuse.Envs), |
| 278 | + latestValue.Fuse.Envs...) |
| 279 | + oldValue.Fuse.Envs = latestValue.Fuse.Envs |
| 280 | + changed = true |
| 281 | + } |
| 282 | + |
| 283 | + // volumeMounts |
| 284 | + if !isSliceEqual(oldValue.Fuse.VolumeMounts, latestValue.Fuse.VolumeMounts) { |
| 285 | + t.Log.Info("syncFuseSpec: volume mounts changed", "old", oldValue.Fuse.VolumeMounts, "new", latestValue.Fuse.VolumeMounts) |
| 286 | + container.VolumeMounts = append( |
| 287 | + utils.GetVolumeMountsDifference(container.VolumeMounts, oldValue.Fuse.VolumeMounts), |
| 288 | + latestValue.Fuse.VolumeMounts...) |
| 289 | + oldValue.Fuse.VolumeMounts = latestValue.Fuse.VolumeMounts |
| 290 | + changed = true |
| 291 | + } |
| 292 | + |
| 293 | + // lifecycle |
| 294 | + if !reflect.DeepEqual(oldValue.Fuse.Lifecycle, latestValue.Fuse.Lifecycle) { |
| 295 | + t.Log.Info("syncFuseSpec: lifecycle changed", "old", oldValue.Fuse.Lifecycle, "new", latestValue.Fuse.Lifecycle) |
| 296 | + container.Lifecycle = latestValue.Fuse.Lifecycle |
| 297 | + oldValue.Fuse.Lifecycle = latestValue.Fuse.Lifecycle |
| 298 | + changed = true |
| 299 | + } |
| 300 | + |
| 301 | + return changed, nil |
| 302 | +} |
| 303 | + |
| 304 | +func composeImage(image, imageTag string) string { |
| 305 | + if len(imageTag) == 0 { |
| 306 | + return image |
| 307 | + } |
| 308 | + return image + ":" + imageTag |
| 309 | +} |
| 310 | + |
| 311 | +// isSliceEqual treats a nil slice and an empty slice as equal, because a value that round trips |
| 312 | +// through the values ConfigMap loses that distinction. |
| 313 | +func isSliceEqual[T any](a, b []T) bool { |
| 314 | + if len(a) == 0 && len(b) == 0 { |
| 315 | + return true |
| 316 | + } |
| 317 | + return reflect.DeepEqual(a, b) |
| 318 | +} |
| 319 | + |
| 320 | +// isMapEqual treats a nil map and an empty map as equal, for the same reason as isSliceEqual. |
| 321 | +func isMapEqual(a, b map[string]string) bool { |
| 322 | + if len(a) == 0 && len(b) == 0 { |
| 323 | + return true |
| 324 | + } |
| 325 | + return reflect.DeepEqual(a, b) |
| 326 | +} |
0 commit comments