Skip to content

Commit 946be3a

Browse files
yevgeny-shnaidmank8s-ci-robot
authored andcommitted
Creating MIC controller
Initial PR for MIc controller. The flow: 1. check the status of pull pods 2. check the status of mbsc 3. create MBSC or pull pods based on the MIC Spec and the statuses collected in steps 1 and 2 The MIC deletion flow is not handled in the PR, will be handled later
1 parent 7bc3457 commit 946be3a

File tree

3 files changed

+1078
-0
lines changed

3 files changed

+1078
-0
lines changed
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
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

Comments
 (0)