|
| 1 | +/* |
| 2 | +Copyright 2022. |
| 3 | +
|
| 4 | +Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | +you may not use this file except in compliance with the License. |
| 6 | +You may obtain a copy of the License at |
| 7 | +
|
| 8 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +
|
| 10 | +Unless required by applicable law or agreed to in writing, software |
| 11 | +distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | +See the License for the specific language governing permissions and |
| 14 | +limitations under the License. |
| 15 | +*/ |
| 16 | + |
| 17 | +package controllers |
| 18 | + |
| 19 | +import ( |
| 20 | + "context" |
| 21 | + "errors" |
| 22 | + "fmt" |
| 23 | + |
| 24 | + kmmv1beta1 "github.com/kubernetes-sigs/kernel-module-management/api/v1beta1" |
| 25 | + "github.com/kubernetes-sigs/kernel-module-management/internal/mbsc" |
| 26 | + "github.com/kubernetes-sigs/kernel-module-management/internal/mic" |
| 27 | + "github.com/kubernetes-sigs/kernel-module-management/internal/utils" |
| 28 | + v1 "k8s.io/api/core/v1" |
| 29 | + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| 30 | + "k8s.io/apimachinery/pkg/runtime" |
| 31 | + ctrl "sigs.k8s.io/controller-runtime" |
| 32 | + "sigs.k8s.io/controller-runtime/pkg/client" |
| 33 | + "sigs.k8s.io/controller-runtime/pkg/reconcile" |
| 34 | +) |
| 35 | + |
| 36 | +const ( |
| 37 | + MICReconcilerName = "MICReconciler" |
| 38 | + |
| 39 | + moduleImageLabelKey = "kmm.node.kubernetes.io/module-image-config" |
| 40 | + imageLabelKey = "kmm.node.kubernetes.io/module-image" |
| 41 | + |
| 42 | + pullerContainerName = "puller" |
| 43 | +) |
| 44 | + |
| 45 | +// micReconciler reconciles a MIC (moduleimagesconfig) object |
| 46 | +type micReconciler struct { |
| 47 | + micReconHelper micReconcilerHelper |
| 48 | + podHelper pullPodManager |
| 49 | +} |
| 50 | + |
| 51 | +func NewMICReconciler(client client.Client, micAPI mic.MIC, mbscAPI mbsc.MBSC, scheme *runtime.Scheme) *micReconciler { |
| 52 | + podHelper := newPullPodManager(client, scheme) |
| 53 | + micReconHelper := newMICReconcilerHelper(client, podHelper, micAPI, mbscAPI, scheme) |
| 54 | + return &micReconciler{ |
| 55 | + micReconHelper: micReconHelper, |
| 56 | + podHelper: podHelper, |
| 57 | + } |
| 58 | +} |
| 59 | + |
| 60 | +// SetupWithManager sets up the controller with the Manager. |
| 61 | +func (r *micReconciler) SetupWithManager(mgr ctrl.Manager) error { |
| 62 | + return ctrl.NewControllerManagedBy(mgr). |
| 63 | + For(&kmmv1beta1.ModuleImagesConfig{}). |
| 64 | + Owns(&v1.Pod{}). |
| 65 | + Owns(&kmmv1beta1.ModuleBuildSignConfig{}). |
| 66 | + Named(MICReconcilerName). |
| 67 | + Complete( |
| 68 | + reconcile.AsReconciler[*kmmv1beta1.ModuleImagesConfig](mgr.GetClient(), r), |
| 69 | + ) |
| 70 | +} |
| 71 | + |
| 72 | +func (r *micReconciler) Reconcile(ctx context.Context, micObj *kmmv1beta1.ModuleImagesConfig) (ctrl.Result, error) { |
| 73 | + res := ctrl.Result{} |
| 74 | + if micObj.GetDeletionTimestamp() != nil { |
| 75 | + // [TODO] delete check image and build/sign pods |
| 76 | + return res, nil |
| 77 | + } |
| 78 | + |
| 79 | + pods, err := r.podHelper.listImagesPullPods(ctx, micObj) |
| 80 | + if err != nil { |
| 81 | + return res, fmt.Errorf("failed to get the image pods for mic %s: %v", micObj.Name, err) |
| 82 | + } |
| 83 | + |
| 84 | + err = r.micReconHelper.updateStatusByPullPods(ctx, micObj, pods) |
| 85 | + if err != nil { |
| 86 | + return res, fmt.Errorf("failed tp update the status for MIC %s based on pull pods: %v", micObj.Name, err) |
| 87 | + } |
| 88 | + |
| 89 | + err = r.micReconHelper.updateStatusByMBSC(ctx, micObj) |
| 90 | + if err != nil { |
| 91 | + return res, fmt.Errorf("failed tp update the status for MIC %s based on builds: %v", micObj.Name, err) |
| 92 | + } |
| 93 | + |
| 94 | + err = r.micReconHelper.processImagesSpecs(ctx, micObj, pods) |
| 95 | + if err != nil { |
| 96 | + return res, fmt.Errorf("failed to process images spec: %v", err) |
| 97 | + } |
| 98 | + return res, nil |
| 99 | +} |
| 100 | + |
| 101 | +//go:generate mockgen -source=mic_reconciler.go -package=controllers -destination=mock_mic_reconciler.go micReconcilerHelper |
| 102 | +type micReconcilerHelper interface { |
| 103 | + updateStatusByPullPods(ctx context.Context, micObj *kmmv1beta1.ModuleImagesConfig, pods []v1.Pod) error |
| 104 | + updateStatusByMBSC(ctx context.Context, micObj *kmmv1beta1.ModuleImagesConfig) error |
| 105 | + processImagesSpecs(ctx context.Context, micObj *kmmv1beta1.ModuleImagesConfig, pullPods []v1.Pod) error |
| 106 | +} |
| 107 | + |
| 108 | +type micReconcilerHelperImpl struct { |
| 109 | + client client.Client |
| 110 | + podHelper pullPodManager |
| 111 | + micHelper mic.MIC |
| 112 | + mbscHelper mbsc.MBSC |
| 113 | + scheme *runtime.Scheme |
| 114 | +} |
| 115 | + |
| 116 | +func newMICReconcilerHelper(client client.Client, |
| 117 | + pullPodHelper pullPodManager, |
| 118 | + micAPI mic.MIC, |
| 119 | + mbscAPI mbsc.MBSC, |
| 120 | + scheme *runtime.Scheme) micReconcilerHelper { |
| 121 | + return &micReconcilerHelperImpl{ |
| 122 | + client: client, |
| 123 | + podHelper: pullPodHelper, |
| 124 | + mbscHelper: mbscAPI, |
| 125 | + micHelper: micAPI, |
| 126 | + scheme: scheme, |
| 127 | + } |
| 128 | +} |
| 129 | + |
| 130 | +func (mrhi *micReconcilerHelperImpl) updateStatusByPullPods(ctx context.Context, micObj *kmmv1beta1.ModuleImagesConfig, pods []v1.Pod) error { |
| 131 | + logger := ctrl.LoggerFrom(ctx) |
| 132 | + if len(pods) == 0 { |
| 133 | + return nil |
| 134 | + } |
| 135 | + |
| 136 | + podsToDelete := make([]v1.Pod, 0, len(pods)) |
| 137 | + patchFrom := client.MergeFrom(micObj.DeepCopy()) |
| 138 | + |
| 139 | + for _, p := range pods { |
| 140 | + image := p.Labels[imageLabelKey] |
| 141 | + imageSpec := mrhi.micHelper.GetModuleImageSpec(micObj, image) |
| 142 | + if imageSpec == nil { |
| 143 | + logger.Info(utils.WarnString("image not present in spec during updateStatusByPods, deleting pod"), "mic", micObj.Name, "image", image) |
| 144 | + podsToDelete = append(podsToDelete, p) |
| 145 | + continue |
| 146 | + } |
| 147 | + phase := p.Status.Phase |
| 148 | + switch phase { |
| 149 | + case v1.PodFailed: |
| 150 | + if imageSpec.Build != nil || imageSpec.Sign != nil { |
| 151 | + logger.Info("failed pod with build or sign spec, updating image status to NeedsBulding") |
| 152 | + mrhi.micHelper.SetImageStatus(micObj, image, kmmv1beta1.ImageNeedsBuilding) |
| 153 | + } else { |
| 154 | + logger.Info(utils.WarnString("failed pod without build or sign spec, shoud not have happened"), "image", image) |
| 155 | + } |
| 156 | + podsToDelete = append(podsToDelete, p) |
| 157 | + case v1.PodSucceeded: |
| 158 | + logger.Info("successful pod, updating image status to ImageExists") |
| 159 | + mrhi.micHelper.SetImageStatus(micObj, image, kmmv1beta1.ImageExists) |
| 160 | + podsToDelete = append(podsToDelete, p) |
| 161 | + } |
| 162 | + } |
| 163 | + // patch the status in the MIC object |
| 164 | + err := mrhi.client.Status().Patch(ctx, micObj, patchFrom) |
| 165 | + if err != nil { |
| 166 | + return fmt.Errorf("failed to patch the status of mic %s: %v", micObj.Name, err) |
| 167 | + } |
| 168 | + |
| 169 | + // deleting pods only after MIC status was patched successfully.otherwise, in the next recon loop |
| 170 | + // we won't have pods to calculate the status |
| 171 | + errs := make([]error, 0, len(podsToDelete)) |
| 172 | + for _, pod := range podsToDelete { |
| 173 | + err = mrhi.podHelper.deletePod(ctx, &pod) |
| 174 | + errs = append(errs, err) |
| 175 | + } |
| 176 | + |
| 177 | + return errors.Join(errs...) |
| 178 | +} |
| 179 | + |
| 180 | +func (mrhi *micReconcilerHelperImpl) updateStatusByMBSC(ctx context.Context, micObj *kmmv1beta1.ModuleImagesConfig) error { |
| 181 | + mbsc, err := mrhi.mbscHelper.Get(ctx, micObj.Name, micObj.Namespace) |
| 182 | + if err != nil { |
| 183 | + return fmt.Errorf("failed to get ModuleBuildSignConfig object %s/%s: %v", micObj.Namespace, micObj.Name, err) |
| 184 | + } |
| 185 | + |
| 186 | + if mbsc == nil { |
| 187 | + return nil |
| 188 | + } |
| 189 | + |
| 190 | + patchFrom := client.MergeFrom(micObj.DeepCopy()) |
| 191 | + for _, imageState := range mbsc.Status.ImagesStates { |
| 192 | + imageSpec := mrhi.micHelper.GetModuleImageSpec(micObj, imageState.Image) |
| 193 | + if imageSpec == nil { |
| 194 | + // image not found in spec, ignore |
| 195 | + continue |
| 196 | + } |
| 197 | + micImageStatus := kmmv1beta1.ImageExists |
| 198 | + if imageState.Status == kmmv1beta1.ImageBuildFailed { |
| 199 | + micImageStatus = kmmv1beta1.ImageDoesNotExist |
| 200 | + } |
| 201 | + mrhi.micHelper.SetImageStatus(micObj, imageState.Image, micImageStatus) |
| 202 | + } |
| 203 | + |
| 204 | + return mrhi.client.Status().Patch(ctx, micObj, patchFrom) |
| 205 | +} |
| 206 | + |
| 207 | +func (mrhi *micReconcilerHelperImpl) processImagesSpecs(ctx context.Context, micObj *kmmv1beta1.ModuleImagesConfig, pullPods []v1.Pod) error { |
| 208 | + errs := make([]error, len(micObj.Spec.Images)) |
| 209 | + for _, imageSpec := range micObj.Spec.Images { |
| 210 | + imageState := mrhi.micHelper.GetImageState(micObj, imageSpec.Image) |
| 211 | + |
| 212 | + switch imageState { |
| 213 | + case "": |
| 214 | + // image State is not set: either new image or pull pod is still running |
| 215 | + if mrhi.podHelper.getPullPodForImage(pullPods, imageSpec.Image) == nil { |
| 216 | + // no pull pod- create it, otherwise we wait for it to finish |
| 217 | + err := mrhi.podHelper.createPullPod(ctx, &imageSpec, micObj) |
| 218 | + errs = append(errs, err) |
| 219 | + } |
| 220 | + case kmmv1beta1.ImageDoesNotExist: |
| 221 | + if imageSpec.Build == nil && imageSpec.Sign == nil { |
| 222 | + break |
| 223 | + } |
| 224 | + fallthrough |
| 225 | + case kmmv1beta1.ImageNeedsBuilding: |
| 226 | + // image needs to be built - patching MBSC |
| 227 | + err := mrhi.mbscHelper.CreateOrPatch(ctx, micObj.Name, micObj.Namespace, &imageSpec, micObj.Spec.ImageRepoSecret, micObj) |
| 228 | + errs = append(errs, err) |
| 229 | + } |
| 230 | + } |
| 231 | + return errors.Join(errs...) |
| 232 | +} |
| 233 | + |
| 234 | +//go:generate mockgen -source=mic_reconciler.go -package=controllers -destination=mock_mic_reconciler.go pullPodManager |
| 235 | +type pullPodManager interface { |
| 236 | + listImagesPullPods(ctx context.Context, micObj *kmmv1beta1.ModuleImagesConfig) ([]v1.Pod, error) |
| 237 | + deletePod(ctx context.Context, pod *v1.Pod) error |
| 238 | + createPullPod(ctx context.Context, imageSpec *kmmv1beta1.ModuleImageSpec, micObj *kmmv1beta1.ModuleImagesConfig) error |
| 239 | + getPullPodForImage(pods []v1.Pod, image string) *v1.Pod |
| 240 | +} |
| 241 | + |
| 242 | +type pullPodManagerImpl struct { |
| 243 | + client client.Client |
| 244 | + scheme *runtime.Scheme |
| 245 | +} |
| 246 | + |
| 247 | +func newPullPodManager(client client.Client, scheme *runtime.Scheme) pullPodManager { |
| 248 | + return &pullPodManagerImpl{ |
| 249 | + client: client, |
| 250 | + scheme: scheme, |
| 251 | + } |
| 252 | +} |
| 253 | + |
| 254 | +func (ppmi *pullPodManagerImpl) listImagesPullPods(ctx context.Context, micObj *kmmv1beta1.ModuleImagesConfig) ([]v1.Pod, error) { |
| 255 | + logger := ctrl.LoggerFrom(ctx).WithValues("mic name", micObj.Name) |
| 256 | + |
| 257 | + pl := v1.PodList{} |
| 258 | + |
| 259 | + hl := client.HasLabels{imageLabelKey} |
| 260 | + ml := client.MatchingLabels{moduleImageLabelKey: micObj.Name} |
| 261 | + |
| 262 | + logger.V(1).Info("Listing mic image Pods") |
| 263 | + |
| 264 | + if err := ppmi.client.List(ctx, &pl, client.InNamespace(micObj.Namespace), hl, ml); err != nil { |
| 265 | + return nil, fmt.Errorf("could not list mic image pods for mic %s: %v", micObj.Name, err) |
| 266 | + } |
| 267 | + |
| 268 | + return pl.Items, nil |
| 269 | +} |
| 270 | + |
| 271 | +func (ppmi *pullPodManagerImpl) deletePod(ctx context.Context, pod *v1.Pod) error { |
| 272 | + logger := ctrl.LoggerFrom(ctx) |
| 273 | + if pod.DeletionTimestamp != nil { |
| 274 | + logger.Info("DeletionTimestamp set, pod is already in deletion", "pod", pod.Name) |
| 275 | + return nil |
| 276 | + } |
| 277 | + if err := ppmi.client.Delete(ctx, pod); client.IgnoreNotFound(err) != nil { |
| 278 | + return fmt.Errorf("failed to delete pull pod %s/%s: %v", pod.Namespace, pod.Name, err) |
| 279 | + } |
| 280 | + return nil |
| 281 | +} |
| 282 | + |
| 283 | +func (ppmi *pullPodManagerImpl) createPullPod(ctx context.Context, imageSpec *kmmv1beta1.ModuleImageSpec, micObj *kmmv1beta1.ModuleImagesConfig) error { |
| 284 | + restartPolicy := v1.RestartPolicyOnFailure |
| 285 | + if imageSpec.Build != nil || imageSpec.Sign != nil { |
| 286 | + restartPolicy = v1.RestartPolicyNever |
| 287 | + } |
| 288 | + var imagePullSecrets []v1.LocalObjectReference |
| 289 | + if micObj.Spec.ImageRepoSecret != nil { |
| 290 | + imagePullSecrets = []v1.LocalObjectReference{*micObj.Spec.ImageRepoSecret} |
| 291 | + } |
| 292 | + |
| 293 | + pullPod := v1.Pod{ |
| 294 | + ObjectMeta: metav1.ObjectMeta{ |
| 295 | + GenerateName: micObj.Name + "-pull-pod-", |
| 296 | + Namespace: micObj.Namespace, |
| 297 | + Labels: map[string]string{ |
| 298 | + moduleImageLabelKey: micObj.Name, |
| 299 | + imageLabelKey: imageSpec.Image, |
| 300 | + }, |
| 301 | + }, |
| 302 | + Spec: v1.PodSpec{ |
| 303 | + Containers: []v1.Container{ |
| 304 | + { |
| 305 | + Name: pullerContainerName, |
| 306 | + Image: imageSpec.Image, |
| 307 | + Command: []string{"/bin/sh", "-c", "exit 0"}, |
| 308 | + }, |
| 309 | + }, |
| 310 | + RestartPolicy: restartPolicy, |
| 311 | + ImagePullSecrets: imagePullSecrets, |
| 312 | + }, |
| 313 | + } |
| 314 | + |
| 315 | + err := ctrl.SetControllerReference(micObj, &pullPod, ppmi.scheme) |
| 316 | + if err != nil { |
| 317 | + return fmt.Errorf("failed to set MIC object %s as owner on pullPod for image %s: %v", micObj.Name, imageSpec.Image, err) |
| 318 | + } |
| 319 | + |
| 320 | + return ppmi.client.Create(ctx, &pullPod) |
| 321 | +} |
| 322 | + |
| 323 | +func (ppmi *pullPodManagerImpl) getPullPodForImage(pods []v1.Pod, image string) *v1.Pod { |
| 324 | + for i, pod := range pods { |
| 325 | + if image == pod.Labels[imageLabelKey] { |
| 326 | + return &pods[i] |
| 327 | + } |
| 328 | + } |
| 329 | + return nil |
| 330 | +} |
0 commit comments