From 2a983b10ce55fde119afb2ce9af4aa22be19ad44 Mon Sep 17 00:00:00 2001 From: cdayz Date: Sat, 2 Mar 2024 16:56:44 +0300 Subject: [PATCH] Add unit tests for reconciller --- go.mod | 13 +- go.sum | 22 +- internal/controller/annotations.go | 55 ++- .../controller/prepullimage_controller.go | 25 +- internal/controller/prepullimage_handlers.go | 12 +- .../controller/prepullimage_reconciller.go | 101 ++-- .../prepullimage_reconciller_test.go | 461 ++++++++++++++++++ 7 files changed, 597 insertions(+), 92 deletions(-) create mode 100644 internal/controller/prepullimage_reconciller_test.go diff --git a/go.mod b/go.mod index 2d769f2..6b38509 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,9 @@ require ( github.com/go-logr/zerologr v1.2.3 github.com/prometheus/client_golang v1.18.0 github.com/rs/zerolog v1.32.0 + github.com/stretchr/testify v1.8.4 go.uber.org/automaxprocs v1.5.3 + golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.29.2 k8s.io/apimachinery v0.29.2 @@ -44,18 +46,19 @@ require ( github.com/onsi/ginkgo/v2 v2.14.0 // indirect github.com/onsi/gomega v1.30.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.45.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.19.0 // indirect + golang.org/x/mod v0.15.0 // indirect + golang.org/x/net v0.21.0 // indirect golang.org/x/oauth2 v0.12.0 // indirect - golang.org/x/sys v0.16.0 // indirect - golang.org/x/term v0.15.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/term v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.16.1 // indirect + golang.org/x/tools v0.18.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index 0b82581..c5789f5 100644 --- a/go.sum +++ b/go.sum @@ -121,17 +121,19 @@ go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnw golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -143,10 +145,10 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -159,8 +161,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= -golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= +golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/controller/annotations.go b/internal/controller/annotations.go index c9dfc6f..f55db16 100644 --- a/internal/controller/annotations.go +++ b/internal/controller/annotations.go @@ -10,47 +10,78 @@ import ( const ControllerAnnotationImageNames = "cdayz.k8s.extensions/image-names" -func GetImageNamesFromAnnotation(obj metav1.Object) ([]string, error) { - var imageNames []string +type ( + ImageName = string + PrePullImageResourceName = string + ImageNamesLookup struct { + Index map[PrePullImageResourceName]ImageName `json:"index"` + InvertedIndex map[ImageName][]PrePullImageResourceName `json:"invIndex"` + } +) + +func GetImageNamesFromAnnotation(obj metav1.Object) (*ImageNamesLookup, error) { + var imageNames ImageNamesLookup if imageNamesListStr, ok := obj.GetAnnotations()[ControllerAnnotationImageNames]; ok { if err := json.Unmarshal([]byte(imageNamesListStr), &imageNames); err != nil { return nil, fmt.Errorf("unmarshal image-names annotation: %w", err) } } - return imageNames, nil + return &imageNames, nil } -func RemoveImageNameFromAnnotation(obj metav1.Object, name string) error { +func RemoveImageNameFromAnnotation(obj metav1.Object, imageName, objectName string) (*ImageNamesLookup, error) { imageNames, err := GetImageNamesFromAnnotation(obj) if err != nil { - return err + return nil, err + } + + oldImageName, existsInIndex := imageNames.Index[objectName] + _, exitstInInvIndex := imageNames.InvertedIndex[oldImageName] + + if !existsInIndex && !exitstInInvIndex { + return imageNames, nil } - imageNames = slices.DeleteFunc(imageNames, func(s string) bool { return s == name }) + delete(imageNames.Index, objectName) + imageNames.InvertedIndex[imageName] = slices.DeleteFunc(imageNames.InvertedIndex[imageName], func(s string) bool { return s == objectName }) + if len(imageNames.InvertedIndex[imageName]) == 0 { + delete(imageNames.InvertedIndex, imageName) + } + imageNames.InvertedIndex[oldImageName] = slices.DeleteFunc(imageNames.InvertedIndex[oldImageName], func(s string) bool { return s == objectName }) + if len(imageNames.InvertedIndex[oldImageName]) == 0 { + delete(imageNames.InvertedIndex, oldImageName) + } val, err := MakeAnnotationValue(imageNames) if err != nil { - return err + return nil, err } ann := obj.GetAnnotations() ann[ControllerAnnotationImageNames] = val obj.SetAnnotations(ann) - return nil + return imageNames, nil } -func AddImageNameToAnnotation(obj metav1.Object, name string) (added bool, err error) { +func AddImageNameToAnnotation(obj metav1.Object, imageName, objectName string) (added bool, err error) { imageNames, err := GetImageNamesFromAnnotation(obj) if err != nil { return false, err } - if slices.Contains(imageNames, name) { + oldImageName, existInIndex := imageNames.Index[objectName] + if existInIndex && oldImageName == imageName && slices.Contains(imageNames.InvertedIndex[imageName], objectName) { return false, nil } - imageNames = append(imageNames, name) + imageNames.Index[objectName] = imageName + imageNames.InvertedIndex[oldImageName] = slices.DeleteFunc(imageNames.InvertedIndex[oldImageName], func(s string) bool { return s == objectName }) + if len(imageNames.InvertedIndex[oldImageName]) == 0 { + delete(imageNames.InvertedIndex, oldImageName) + } + + imageNames.InvertedIndex[imageName] = append(imageNames.InvertedIndex[imageName], objectName) val, err := MakeAnnotationValue(imageNames) if err != nil { return false, err @@ -63,7 +94,7 @@ func AddImageNameToAnnotation(obj metav1.Object, name string) (added bool, err e return true, nil } -func MakeAnnotationValue(imageNames []string) (string, error) { +func MakeAnnotationValue(imageNames *ImageNamesLookup) (string, error) { imageNamesListBytes, err := json.Marshal(imageNames) if err != nil { return "", fmt.Errorf("marshal image-name annotation: %w", err) diff --git a/internal/controller/prepullimage_controller.go b/internal/controller/prepullimage_controller.go index 452ff34..fa73c7b 100644 --- a/internal/controller/prepullimage_controller.go +++ b/internal/controller/prepullimage_controller.go @@ -23,8 +23,6 @@ import ( ) type PrePullImageController struct { - reconcileConfig *PrePullImageReconcilerConfig - kubeClient kubernetes.Interface imClient imclientset.Interface @@ -39,6 +37,8 @@ type PrePullImageController struct { workers uint32 queueList []workqueue.RateLimitingInterface + + reconciller prePullImageReconciller } func NewPrePullImageController( @@ -61,8 +61,6 @@ func NewPrePullImageController( podLister := podInformer.Lister() cc := PrePullImageController{ - reconcileConfig: reconcileConfig, - kubeClient: kubeClient, imClient: imClient, @@ -77,6 +75,12 @@ func NewPrePullImageController( workers: workers, queueList: queueList, + + reconciller: prePullImageReconciller{ + reconcileConfig: reconcileConfig, + kubeClient: kubeClient, + imClient: imClient, + }, } _, _ = cc.prePullImageInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ @@ -173,7 +177,7 @@ func (cc *PrePullImageController) processNextReq(ctx context.Context, count uint queue := cc.queueList[count] obj, shutdown := queue.Get() if shutdown { - klog.Errorf("Fail to pop item from queue") + klog.Errorf("fail to pop item from queue") return false } @@ -190,12 +194,19 @@ func (cc *PrePullImageController) processNextReq(ctx context.Context, count uint klog.V(3).Infof("try to handle request <%v>", req) - _, err := cc.Reconcile(ctx, req) + res, err := cc.reconciller.Reconcile(ctx, req) if err != nil { - klog.V(2).Infof("Failed to handle PrePullImage<%s/%s>: %v", req.Namespace, req.Name, err) + klog.V(2).Infof("failed to handle PrePullImage<%s/%s>: %v", req.Namespace, req.Name, err) // If any error, requeue it. queue.AddRateLimited(req) return true + } else if res.Requeue { + if res.RequeueAfter != 0 { + queue.AddAfter(req, res.RequeueAfter) + } else { + queue.AddRateLimited(req) + } + return true } // If no error, forget it. diff --git a/internal/controller/prepullimage_handlers.go b/internal/controller/prepullimage_handlers.go index 99714a6..a7cf9c7 100644 --- a/internal/controller/prepullimage_handlers.go +++ b/internal/controller/prepullimage_handlers.go @@ -69,8 +69,8 @@ func (cc *PrePullImageController) addPod(obj interface{}) { return } - for _, imageName := range names { - name := ReconcileRequest{Namespace: pod.Namespace, Name: imageName, PodName: &pod.Name} + for name := range names.Index { + name := ReconcileRequest{Namespace: pod.Namespace, Name: name, PodName: &pod.Name} queue := cc.getWorkerQueue(name.Key()) queue.Add(name) } @@ -105,8 +105,8 @@ func (cc *PrePullImageController) updatePod(oldObj, newObj interface{}) { return } - for _, imageName := range names { - name := ReconcileRequest{Namespace: newPod.Namespace, Name: imageName, PodName: &newPod.Name} + for name := range names.Index { + name := ReconcileRequest{Namespace: newPod.Namespace, Name: name, PodName: &newPod.Name} queue := cc.getWorkerQueue(name.Key()) queue.Add(name) } @@ -138,8 +138,8 @@ func (cc *PrePullImageController) deletePod(obj interface{}) { return } - for _, imageName := range names { - name := ReconcileRequest{Namespace: pod.Namespace, Name: imageName, PodName: &pod.Name} + for name := range names.Index { + name := ReconcileRequest{Namespace: pod.Namespace, Name: name, PodName: &pod.Name} queue := cc.getWorkerQueue(name.Key()) queue.Add(name) } diff --git a/internal/controller/prepullimage_reconciller.go b/internal/controller/prepullimage_reconciller.go index afb31a0..c23c8d6 100644 --- a/internal/controller/prepullimage_reconciller.go +++ b/internal/controller/prepullimage_reconciller.go @@ -12,10 +12,12 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/client-go/kubernetes" "github.com/Cdayz/k8s-image-pre-puller/internal/controller/utils" "github.com/Cdayz/k8s-image-pre-puller/internal/names" imagesv1 "github.com/Cdayz/k8s-image-pre-puller/pkg/apis/images/v1" + imclientset "github.com/Cdayz/k8s-image-pre-puller/pkg/client/clientset/versioned" ) const ( @@ -32,7 +34,13 @@ const ( var MaxUnavailablePodsOfDaemonSetDuringRollingUpdate = intstr.FromString("100%") -func (r *PrePullImageController) Reconcile(ctx context.Context, req ReconcileRequest) (ControllerResult, error) { +type prePullImageReconciller struct { + reconcileConfig *PrePullImageReconcilerConfig + kubeClient kubernetes.Interface + imClient imclientset.Interface +} + +func (r *prePullImageReconciller) Reconcile(ctx context.Context, req ReconcileRequest) (ControllerResult, error) { prePullImage, err := r.imClient.ImagesV1().PrePullImages(req.Namespace).Get(ctx, req.Name, metav1.GetOptions{}) if err != nil { if apierrors.IsNotFound(err) { @@ -72,7 +80,7 @@ func (r *PrePullImageController) Reconcile(ctx context.Context, req ReconcileReq return ControllerResult{}, nil } -func (r *PrePullImageController) removeFromPrePulling(ctx context.Context, prePullImage *imagesv1.PrePullImage) error { +func (r *prePullImageReconciller) removeFromPrePulling(ctx context.Context, prePullImage *imagesv1.PrePullImage) error { daemonSet, err := r.getCurrentPrePullingDaemonset(ctx, prePullImage) if err != nil { return fmt.Errorf("get current pre-pulling daemonset: %w", err) @@ -81,15 +89,19 @@ func (r *PrePullImageController) removeFromPrePulling(ctx context.Context, prePu return nil } - if err := RemoveImageNameFromAnnotation(daemonSet, prePullImage.Name); err != nil { + imageNamesLookup, err := RemoveImageNameFromAnnotation(daemonSet, prePullImage.Spec.Image, prePullImage.Name) + if err != nil { return fmt.Errorf("remove image names from daemonset annotations: %w", err) } daemonSet.Spec.Template.Annotations = daemonSet.Annotations // WARN: We should have same annotations on pod - daemonSet.Spec.Template.Spec.InitContainers = slices.DeleteFunc( - daemonSet.Spec.Template.Spec.InitContainers, - func(ctr corev1.Container) bool { return ctr.Image == prePullImage.Spec.Image }, - ) + + if len(imageNamesLookup.InvertedIndex[prePullImage.Spec.Image]) == 0 { + daemonSet.Spec.Template.Spec.InitContainers = slices.DeleteFunc( + daemonSet.Spec.Template.Spec.InitContainers, + func(ctr corev1.Container) bool { return ctr.Image == prePullImage.Spec.Image }, + ) + } if len(daemonSet.Spec.Template.Spec.InitContainers) > 0 { _, err := r.kubeClient.AppsV1().DaemonSets(daemonSet.Namespace).Update(ctx, daemonSet, metav1.UpdateOptions{}) if err != nil { @@ -105,7 +117,7 @@ func (r *PrePullImageController) removeFromPrePulling(ctx context.Context, prePu return nil } -func (r *PrePullImageController) ensurePrePulling(ctx context.Context, prePullImage *imagesv1.PrePullImage) error { +func (r *prePullImageReconciller) ensurePrePulling(ctx context.Context, prePullImage *imagesv1.PrePullImage) error { daemonSet, err := r.getCurrentPrePullingDaemonset(ctx, prePullImage) if err != nil { return fmt.Errorf("get current pre-pulling daemonset: %w", err) @@ -134,8 +146,8 @@ func (r *PrePullImageController) ensurePrePulling(ctx context.Context, prePullIm return nil } -func (r *PrePullImageController) getCurrentPrePullingDaemonset(ctx context.Context, prePullImage *imagesv1.PrePullImage) (*appsv1.DaemonSet, error) { - daemonSet, err := r.kubeClient.AppsV1().DaemonSets(prePullImage.Namespace).Get(ctx, r.makeDaemonSetName(prePullImage), metav1.GetOptions{}) +func (r *prePullImageReconciller) getCurrentPrePullingDaemonset(ctx context.Context, prePullImage *imagesv1.PrePullImage) (*appsv1.DaemonSet, error) { + daemonSet, err := r.kubeClient.AppsV1().DaemonSets(prePullImage.Namespace).Get(ctx, makeDaemonSetName(prePullImage), metav1.GetOptions{}) if err != nil { if apierrors.IsNotFound(err) { return nil, nil @@ -146,13 +158,16 @@ func (r *PrePullImageController) getCurrentPrePullingDaemonset(ctx context.Conte return daemonSet, nil } -func (r *PrePullImageController) createDaemonSetForPrePulling(ctx context.Context, prePullImage *imagesv1.PrePullImage) (*appsv1.DaemonSet, error) { +func (r *prePullImageReconciller) createDaemonSetForPrePulling(ctx context.Context, prePullImage *imagesv1.PrePullImage) (*appsv1.DaemonSet, error) { labels := map[string]string{ - DaemonSetLabelNodeSelectorHash: r.nodeSelectorHash(prePullImage), + DaemonSetLabelNodeSelectorHash: nodeSelectorHash(prePullImage), ControllerLabelPurposeName: ControllerLabelPurposeValue, } - annVal, err := MakeAnnotationValue([]string{prePullImage.Name}) + annVal, err := MakeAnnotationValue(&ImageNamesLookup{ + Index: map[string]string{prePullImage.Name: prePullImage.Spec.Image}, + InvertedIndex: map[string][]string{prePullImage.Spec.Image: {prePullImage.Name}}, + }) if err != nil { return nil, fmt.Errorf("make index annotation: %w", err) } @@ -171,7 +186,7 @@ func (r *PrePullImageController) createDaemonSetForPrePulling(ctx context.Contex daemonSet := &appsv1.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ - Name: r.makeDaemonSetName(prePullImage), + Name: makeDaemonSetName(prePullImage), Namespace: prePullImage.Namespace, Labels: labels, Annotations: annotations, @@ -209,39 +224,25 @@ func (r *PrePullImageController) createDaemonSetForPrePulling(ctx context.Contex return daemonSet, nil } -func (r *PrePullImageController) ensurePrePullContainerInDaemonSet(ctx context.Context, daemonSet *appsv1.DaemonSet, prePullImage *imagesv1.PrePullImage) (*appsv1.DaemonSet, error) { - prePullContainerName := r.makePrePullContainerName(prePullImage) - - var ( - declaredInSpec bool - specHasSameImage bool - ) - for _, ctr := range daemonSet.Spec.Template.Spec.InitContainers { - if ctr.Name == prePullContainerName { - declaredInSpec = true - specHasSameImage = ctr.Image == prePullImage.Spec.Image - break - } - } +func (r *prePullImageReconciller) ensurePrePullContainerInDaemonSet(ctx context.Context, daemonSet *appsv1.DaemonSet, prePullImage *imagesv1.PrePullImage) (*appsv1.DaemonSet, error) { + containerIndexInSpec := slices.IndexFunc(daemonSet.Spec.Template.Spec.InitContainers, func(ctr corev1.Container) bool { + return ctr.Image == prePullImage.Spec.Image + }) + declaredInSpec := containerIndexInSpec != -1 - addedToAnnotation, err := AddImageNameToAnnotation(daemonSet, prePullImage.Name) + addedToAnnotation, err := AddImageNameToAnnotation(daemonSet, prePullImage.Spec.Image, prePullImage.Name) if err != nil { return nil, fmt.Errorf("add image-name to daemonset annotation: %w", err) } - if declaredInSpec && specHasSameImage && !addedToAnnotation { + if declaredInSpec && !addedToAnnotation { return daemonSet, nil } daemonSet.Spec.Template.Annotations = daemonSet.Annotations // WARN: We should have same annotations on pod - indexInSpec := slices.IndexFunc(daemonSet.Spec.Template.Spec.InitContainers, func(ctr corev1.Container) bool { - return ctr.Name == prePullContainerName - }) - if indexInSpec == -1 { + if containerIndexInSpec == -1 { daemonSet.Spec.Template.Spec.InitContainers = append(daemonSet.Spec.Template.Spec.InitContainers, r.createPrePullContainer(prePullImage)) - } else { - daemonSet.Spec.Template.Spec.InitContainers[indexInSpec].Image = prePullImage.Spec.Image } daemonSet, err = r.kubeClient.AppsV1().DaemonSets(daemonSet.Namespace).Update(ctx, daemonSet, metav1.UpdateOptions{}) @@ -252,15 +253,15 @@ func (r *PrePullImageController) ensurePrePullContainerInDaemonSet(ctx context.C return daemonSet, nil } -func (r *PrePullImageController) ensureDaemonsetPrepullOnlyImagesFromAnnotation(ctx context.Context, daemonSet *appsv1.DaemonSet) (*appsv1.DaemonSet, error) { +func (r *prePullImageReconciller) ensureDaemonsetPrepullOnlyImagesFromAnnotation(ctx context.Context, daemonSet *appsv1.DaemonSet) (*appsv1.DaemonSet, error) { imageNames, err := GetImageNamesFromAnnotation(daemonSet) if err != nil { return nil, fmt.Errorf("get image names from daemonset annotation: %w", err) } prePullContainerNames := map[string]bool{} - for _, name := range imageNames { - prePullContainerNames[r.makePrePullContainerNameFromString(name)] = true + for imageName := range imageNames.InvertedIndex { + prePullContainerNames[makePrePullContainerNameFromString(imageName)] = true } initContainers := []corev1.Container{} @@ -287,7 +288,7 @@ func (r *PrePullImageController) ensureDaemonsetPrepullOnlyImagesFromAnnotation( return daemonSet, nil } -func (r *PrePullImageController) ensureDaemonsetHasProperTolerations(ctx context.Context, daemonSet *appsv1.DaemonSet, prePullImage *imagesv1.PrePullImage) (*appsv1.DaemonSet, error) { +func (r *prePullImageReconciller) ensureDaemonsetHasProperTolerations(ctx context.Context, daemonSet *appsv1.DaemonSet, prePullImage *imagesv1.PrePullImage) (*appsv1.DaemonSet, error) { requiredTolerations, err := r.getTolerationsByNodeSelector(ctx, prePullImage.Spec.NodeSelector) if err != nil { return nil, fmt.Errorf("list tolerations by node selector: %w", err) @@ -330,7 +331,7 @@ func (r *PrePullImageController) ensureDaemonsetHasProperTolerations(ctx context return daemonSet, nil } -func (r *PrePullImageController) getTolerationsByNodeSelector(ctx context.Context, nodeSelector map[string]string) ([]corev1.Toleration, error) { +func (r *prePullImageReconciller) getTolerationsByNodeSelector(ctx context.Context, nodeSelector map[string]string) ([]corev1.Toleration, error) { nodes, err := r.kubeClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{ LabelSelector: labels.SelectorFromSet(labels.Set(nodeSelector)).String(), }) @@ -366,9 +367,9 @@ func (r *PrePullImageController) getTolerationsByNodeSelector(ctx context.Contex return tolerations, nil } -func (r *PrePullImageController) createPrePullContainer(prePullImage *imagesv1.PrePullImage) corev1.Container { +func (r *prePullImageReconciller) createPrePullContainer(prePullImage *imagesv1.PrePullImage) corev1.Container { return corev1.Container{ - Name: r.makePrePullContainerName(prePullImage), + Name: makePrePullContainerNameFromString(prePullImage.Spec.Image), Image: prePullImage.Spec.Image, Command: r.reconcileConfig.PrePullContainer.Command, Args: r.reconcileConfig.PrePullContainer.Args, @@ -376,7 +377,7 @@ func (r *PrePullImageController) createPrePullContainer(prePullImage *imagesv1.P } } -func (r *PrePullImageController) createMainContainer() corev1.Container { +func (r *prePullImageReconciller) createMainContainer() corev1.Container { return corev1.Container{ Name: r.reconcileConfig.MainContainer.Name, Image: r.reconcileConfig.MainContainer.Image, @@ -386,20 +387,16 @@ func (r *PrePullImageController) createMainContainer() corev1.Container { } } -func (r *PrePullImageController) makePrePullContainerName(prePullImage *imagesv1.PrePullImage) string { - return r.makePrePullContainerNameFromString(prePullImage.Name) -} - -func (r *PrePullImageController) makePrePullContainerNameFromString(name string) string { +func makePrePullContainerNameFromString(name string) string { imageHashStr := names.StringHash(name) return fmt.Sprintf("pre-pull-%d", imageHashStr) } -func (r *PrePullImageController) nodeSelectorHash(prePullImage *imagesv1.PrePullImage) string { +func nodeSelectorHash(prePullImage *imagesv1.PrePullImage) string { return strconv.FormatUint(uint64(names.StringMapHash(prePullImage.Spec.NodeSelector)), 10) } -func (r *PrePullImageController) makeDaemonSetName(prePullImage *imagesv1.PrePullImage) string { - nodeSelectorHashStr := r.nodeSelectorHash(prePullImage) +func makeDaemonSetName(prePullImage *imagesv1.PrePullImage) string { + nodeSelectorHashStr := nodeSelectorHash(prePullImage) return names.MakeK8SName([]string{DaemonSetNamePrefix, nodeSelectorHashStr}, names.IncludeCRC(true), names.MaxLength(63)) } diff --git a/internal/controller/prepullimage_reconciller_test.go b/internal/controller/prepullimage_reconciller_test.go new file mode 100644 index 0000000..9fdd10f --- /dev/null +++ b/internal/controller/prepullimage_reconciller_test.go @@ -0,0 +1,461 @@ +package controller + +import ( + "context" + "slices" + "testing" + + "github.com/stretchr/testify/suite" + "golang.org/x/exp/maps" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" + + imagesv1 "github.com/Cdayz/k8s-image-pre-puller/pkg/apis/images/v1" + imclientset "github.com/Cdayz/k8s-image-pre-puller/pkg/client/clientset/versioned" + imclientsetfake "github.com/Cdayz/k8s-image-pre-puller/pkg/client/clientset/versioned/fake" +) + +// In order for 'go test' to run this suite, we need to create +// a normal test function and pass our suite to suite.Run +func TestPrePullImageReconcillerTestSuite(t *testing.T) { + suite.Run(t, new(PrePullImageReconcillerTestSuite)) +} + +type PrePullImageReconcillerTestSuite struct { + suite.Suite + + k8sClient kubernetes.Interface + imClient imclientset.Interface + reconcillerConfig *PrePullImageReconcilerConfig +} + +func (s *PrePullImageReconcillerTestSuite) SetupTest() { + s.k8sClient = fake.NewSimpleClientset() + s.imClient = imclientsetfake.NewSimpleClientset() + s.reconcillerConfig = &PrePullImageReconcilerConfig{ + MainContainer: ContainerConfig{ + Name: "main", + Image: "busybox", + Command: []string{"/bin/sh"}, + Args: []string{"-c", "'sleep inf'"}, + Resources: corev1.ResourceRequirements{}, + }, + PrePullContainer: ContainerConfig{ + Command: []string{"/bin/sh"}, + Args: []string{"-c", "'exit 0'"}, + Resources: corev1.ResourceRequirements{}, + }, + ImagePullSecretNames: []string{"artifactory-cred-secret"}, + } +} + +func (s *PrePullImageReconcillerTestSuite) validateDaemonSetMatchesPrePullImage(daemonSet *appsv1.DaemonSet, prePullImage *imagesv1.PrePullImage) { + s.T().Helper() + + s.Require().Equal(makeDaemonSetName(prePullImage), daemonSet.Name) + s.Require().GreaterOrEqual(len(daemonSet.Spec.Template.Spec.InitContainers), 1) + + imageNames, err := GetImageNamesFromAnnotation(daemonSet) + s.Require().NoError(err) + s.Require().Contains(imageNames.Index, prePullImage.Name) + s.Require().Equal(imageNames.Index[prePullImage.Name], prePullImage.Spec.Image) + s.Require().Contains(imageNames.InvertedIndex, prePullImage.Spec.Image) + s.Require().Contains(imageNames.InvertedIndex[prePullImage.Spec.Image], prePullImage.Name) + + imageNames, err = GetImageNamesFromAnnotation(&daemonSet.Spec.Template) + s.Require().NoError(err) + s.Require().Contains(imageNames.Index, prePullImage.Name) + s.Require().Equal(imageNames.Index[prePullImage.Name], prePullImage.Spec.Image) + s.Require().Contains(imageNames.InvertedIndex, prePullImage.Spec.Image) + s.Require().Contains(imageNames.InvertedIndex[prePullImage.Spec.Image], prePullImage.Name) + + idx := slices.IndexFunc(daemonSet.Spec.Template.Spec.InitContainers, func(c corev1.Container) bool { + return c.Image == prePullImage.Spec.Image + }) + s.Require().NotEqual(-1, idx, "init container should be defined inside daemonset") +} + +func (s *PrePullImageReconcillerTestSuite) validateDaemonSetContainsOnlyProperContainers(daemonSet *appsv1.DaemonSet) { + s.T().Helper() + + imageNames, err := GetImageNamesFromAnnotation(daemonSet) + s.Require().NoError(err) + + imagesList, err := s.imClient.ImagesV1().PrePullImages(daemonSet.Namespace).List(context.Background(), metav1.ListOptions{}) + s.Require().NoError(err) + + matchingImages := []imagesv1.PrePullImage{} + matchingImagesLookup := ImageNamesLookup{ + Index: map[string]string{}, + InvertedIndex: map[string][]string{}, + } + for _, image := range imagesList.Items { + if makeDaemonSetName(&image) != daemonSet.Name { + continue + } + if !image.ObjectMeta.DeletionTimestamp.IsZero() { + continue + } + matchingImages = append(matchingImages, image) + matchingImagesLookup.InvertedIndex[image.Spec.Image] = append(matchingImagesLookup.InvertedIndex[image.Spec.Image], image.Name) + } + + // Ensure annotation matches real data + s.Require().Lenf(matchingImagesLookup.InvertedIndex, len(imageNames.InvertedIndex), "%v", imageNames.InvertedIndex) + s.Require().ElementsMatch(maps.Keys(imageNames.InvertedIndex), maps.Keys(matchingImagesLookup.InvertedIndex)) + for imageName, objectNames := range imageNames.InvertedIndex { + s.Require().ElementsMatchf(objectNames, matchingImagesLookup.InvertedIndex[imageName], "objectNames by key %s didnt match in invertedIndex", imageName) + } + + // Ensure daemonset really matches real data + for _, img := range matchingImages { + s.validateDaemonSetMatchesPrePullImage(daemonSet, &img) + } + + // Ensure only containers related to annotation declared + for _, ctr := range daemonSet.Spec.Template.Spec.InitContainers { + s.Require().Contains(imageNames.InvertedIndex, ctr.Image) + } +} + +func (s *PrePullImageReconcillerTestSuite) TestWhenObjectDoesNotExistsNothingChanged() { + r := prePullImageReconciller{ + reconcileConfig: s.reconcillerConfig, + kubeClient: s.k8sClient, + imClient: s.imClient, + } + + result, err := r.Reconcile(context.Background(), ReconcileRequest{ + Name: "", + Namespace: "", + }) + s.Require().NoError(err) + s.Require().Equal(ControllerResult{}, result) + + daemonSets, err := s.k8sClient.AppsV1().DaemonSets(metav1.NamespaceAll).List(context.Background(), metav1.ListOptions{}) + s.Require().NoError(err) + s.Require().Len(daemonSets.Items, 0, "no daemonsets should be created") +} + +func (s *PrePullImageReconcillerTestSuite) TestWhenNewObjectComesDaemonsetCreated() { + r := prePullImageReconciller{ + reconcileConfig: s.reconcillerConfig, + kubeClient: s.k8sClient, + imClient: s.imClient, + } + + namespace := "default" + prePullImage, err := s.imClient.ImagesV1().PrePullImages(namespace).Create(context.Background(), &imagesv1.PrePullImage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pre-pull-1", + Namespace: namespace, + }, + Spec: imagesv1.PrePullImageSpec{ + Image: "my-image:v0.0.1", + }, + }, metav1.CreateOptions{}) + s.Require().NoError(err) + + result, err := r.Reconcile(context.Background(), ReconcileRequest{ + Name: prePullImage.Name, + Namespace: prePullImage.Namespace, + }) + s.Require().NoError(err) + s.Require().Equal(ControllerResult{}, result) + + daemonSets, err := s.k8sClient.AppsV1().DaemonSets(prePullImage.Namespace).List(context.Background(), metav1.ListOptions{}) + s.Require().NoError(err) + s.Require().Len(daemonSets.Items, 1, "daemonset should be created") + s.validateDaemonSetContainsOnlyProperContainers(&daemonSets.Items[0]) +} + +func (s *PrePullImageReconcillerTestSuite) TestWhenLastObjectRelatedToDaemonSetDeletesDaemonsetDeleted() { + r := prePullImageReconciller{ + reconcileConfig: s.reconcillerConfig, + kubeClient: s.k8sClient, + imClient: s.imClient, + } + + // Create object and daemonset + var prePullImage *imagesv1.PrePullImage + { + var err error + namespace := "default" + prePullImage, err = s.imClient.ImagesV1().PrePullImages(namespace).Create(context.Background(), &imagesv1.PrePullImage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pre-pull-1", + Namespace: namespace, + }, + Spec: imagesv1.PrePullImageSpec{ + Image: "my-image:v0.0.1", + }, + }, metav1.CreateOptions{}) + s.Require().NoError(err) + + result, err := r.Reconcile(context.Background(), ReconcileRequest{ + Name: prePullImage.Name, + Namespace: prePullImage.Namespace, + }) + s.Require().NoError(err) + s.Require().Equal(ControllerResult{}, result) + + daemonSets, err := s.k8sClient.AppsV1().DaemonSets(prePullImage.Namespace).List(context.Background(), metav1.ListOptions{}) + s.Require().NoError(err) + s.Require().Len(daemonSets.Items, 1, "daemonset should be created") + s.validateDaemonSetContainsOnlyProperContainers(&daemonSets.Items[0]) + } + + // Refresh object, we need to update from latest changes + prePullImage, err := s.imClient.ImagesV1().PrePullImages(prePullImage.Namespace).Get(context.Background(), prePullImage.Name, metav1.GetOptions{}) + s.Require().NoError(err) + + // Mark object for deletion + now := metav1.Now() + prePullImage.DeletionTimestamp = &now + prePullImage, err = s.imClient.ImagesV1().PrePullImages(prePullImage.Namespace).Update(context.Background(), prePullImage, metav1.UpdateOptions{}) + s.Require().NoError(err) + + result, err := r.Reconcile(context.Background(), ReconcileRequest{ + Name: prePullImage.Name, + Namespace: prePullImage.Namespace, + }) + s.Require().NoError(err) + s.Require().Equal(ControllerResult{}, result) + + daemonSets, err := s.k8sClient.AppsV1().DaemonSets(prePullImage.Namespace).List(context.Background(), metav1.ListOptions{}) + s.Require().NoError(err) + s.Require().Len(daemonSets.Items, 0, "daemonset should be deleted") +} + +func (s *PrePullImageReconcillerTestSuite) TestWhenObjectSpecUpdatedDaemonsetAlsoUpdated() { + r := prePullImageReconciller{ + reconcileConfig: s.reconcillerConfig, + kubeClient: s.k8sClient, + imClient: s.imClient, + } + + // Prepare stage + namespace := "default" + prePullImage, err := s.imClient.ImagesV1().PrePullImages(namespace).Create(context.Background(), &imagesv1.PrePullImage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pre-pull-1", + Namespace: namespace, + }, + Spec: imagesv1.PrePullImageSpec{ + Image: "my-image:v0.0.1", + }, + }, metav1.CreateOptions{}) + s.Require().NoError(err) + + result, err := r.Reconcile(context.Background(), ReconcileRequest{ + Name: prePullImage.Name, + Namespace: prePullImage.Namespace, + }) + s.Require().NoError(err) + s.Require().Equal(ControllerResult{}, result) + + daemonSets, err := s.k8sClient.AppsV1().DaemonSets(prePullImage.Namespace).List(context.Background(), metav1.ListOptions{}) + s.Require().NoError(err) + s.Require().Len(daemonSets.Items, 1, "daemonset should be created") + s.validateDaemonSetContainsOnlyProperContainers(&daemonSets.Items[0]) + + // Update + prePullImage.Spec.Image = "my-image-2:v0.0.2" + prePullImage, err = s.imClient.ImagesV1().PrePullImages(namespace).Update(context.Background(), prePullImage, metav1.UpdateOptions{}) + s.Require().NoError(err) + + // Reconcile updated object + result, err = r.Reconcile(context.Background(), ReconcileRequest{ + Name: prePullImage.Name, + Namespace: prePullImage.Namespace, + }) + s.Require().NoError(err) + s.Require().Equal(ControllerResult{}, result) + + // Assert updated are commited + daemonSets, err = s.k8sClient.AppsV1().DaemonSets(prePullImage.Namespace).List(context.Background(), metav1.ListOptions{}) + s.Require().NoError(err) + s.Require().Len(daemonSets.Items, 1, "daemonset should be created") + s.validateDaemonSetContainsOnlyProperContainers(&daemonSets.Items[0]) +} + +func (s *PrePullImageReconcillerTestSuite) TestWhenObjectSpecUpdatedDaemonsetAlsoUpdatedManyObjectDifferentImages() { + r := prePullImageReconciller{ + reconcileConfig: s.reconcillerConfig, + kubeClient: s.k8sClient, + imClient: s.imClient, + } + + // Prepare stage + var ( + prePullImageFirst, prePullImageSecond *imagesv1.PrePullImage + err error + ) + { + namespace := "default" + prePullImageFirst, err = s.imClient.ImagesV1().PrePullImages(namespace).Create(context.Background(), &imagesv1.PrePullImage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pre-pull-1", + Namespace: namespace, + }, + Spec: imagesv1.PrePullImageSpec{ + Image: "my-image:v0.0.1", + }, + }, metav1.CreateOptions{}) + s.Require().NoError(err) + + result, err := r.Reconcile(context.Background(), ReconcileRequest{ + Name: prePullImageFirst.Name, + Namespace: prePullImageFirst.Namespace, + }) + s.Require().NoError(err) + s.Require().Equal(ControllerResult{}, result) + + daemonSets, err := s.k8sClient.AppsV1().DaemonSets(prePullImageFirst.Namespace).List(context.Background(), metav1.ListOptions{}) + s.Require().NoError(err) + s.Require().Len(daemonSets.Items, 1, "daemonset should be created") + s.validateDaemonSetContainsOnlyProperContainers(&daemonSets.Items[0]) + } + + // Prepare stage - second object with different image + { + namespace := "default" + prePullImageSecond, err = s.imClient.ImagesV1().PrePullImages(namespace).Create(context.Background(), &imagesv1.PrePullImage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pre-pull-2", + Namespace: namespace, + }, + Spec: imagesv1.PrePullImageSpec{ + Image: "my-image-2:v0.0.1", + }, + }, metav1.CreateOptions{}) + s.Require().NoError(err) + + result, err := r.Reconcile(context.Background(), ReconcileRequest{ + Name: prePullImageSecond.Name, + Namespace: prePullImageSecond.Namespace, + }) + s.Require().NoError(err) + s.Require().Equal(ControllerResult{}, result) + + daemonSets, err := s.k8sClient.AppsV1().DaemonSets(prePullImageSecond.Namespace).List(context.Background(), metav1.ListOptions{}) + s.Require().NoError(err) + s.Require().Len(daemonSets.Items, 1, "daemonset should be updated") + s.validateDaemonSetContainsOnlyProperContainers(&daemonSets.Items[0]) + } + + // Update first image - change to has same image as second + prePullImageFirst.Spec.Image = "my-image-2:v0.0.2" + prePullImageFirst, err = s.imClient.ImagesV1().PrePullImages(prePullImageFirst.Namespace).Update(context.Background(), prePullImageFirst, metav1.UpdateOptions{}) + s.Require().NoError(err) + + // Reconcile updated object + result, err := r.Reconcile(context.Background(), ReconcileRequest{ + Name: prePullImageFirst.Name, + Namespace: prePullImageFirst.Namespace, + }) + s.Require().NoError(err) + s.Require().Equal(ControllerResult{}, result) + + // Assert updated are commited + daemonSets, err := s.k8sClient.AppsV1().DaemonSets(prePullImageFirst.Namespace).List(context.Background(), metav1.ListOptions{}) + s.Require().NoError(err) + s.Require().Len(daemonSets.Items, 1, "daemonset should be updated") + s.validateDaemonSetContainsOnlyProperContainers(&daemonSets.Items[0]) +} + +func (s *PrePullImageReconcillerTestSuite) TestWhenObjectDeletedDaemonsetAlsoUpdated() { + r := prePullImageReconciller{ + reconcileConfig: s.reconcillerConfig, + kubeClient: s.k8sClient, + imClient: s.imClient, + } + + // Prepare stage + var ( + prePullImageFirst, prePullImageSecond *imagesv1.PrePullImage + err error + ) + { + namespace := "default" + prePullImageFirst, err = s.imClient.ImagesV1().PrePullImages(namespace).Create(context.Background(), &imagesv1.PrePullImage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pre-pull-1", + Namespace: namespace, + }, + Spec: imagesv1.PrePullImageSpec{ + Image: "my-image:v0.0.1", + }, + }, metav1.CreateOptions{}) + s.Require().NoError(err) + + result, err := r.Reconcile(context.Background(), ReconcileRequest{ + Name: prePullImageFirst.Name, + Namespace: prePullImageFirst.Namespace, + }) + s.Require().NoError(err) + s.Require().Equal(ControllerResult{}, result) + + // Refresh object, we need to update from latest changes + prePullImageFirst, err = s.imClient.ImagesV1().PrePullImages(prePullImageFirst.Namespace).Get(context.Background(), prePullImageFirst.Name, metav1.GetOptions{}) + s.Require().NoError(err) + + daemonSets, err := s.k8sClient.AppsV1().DaemonSets(prePullImageFirst.Namespace).List(context.Background(), metav1.ListOptions{}) + s.Require().NoError(err) + s.Require().Len(daemonSets.Items, 1, "daemonset should be created") + s.validateDaemonSetContainsOnlyProperContainers(&daemonSets.Items[0]) + } + + // Prepare stage - second object with different image + { + namespace := "default" + prePullImageSecond, err = s.imClient.ImagesV1().PrePullImages(namespace).Create(context.Background(), &imagesv1.PrePullImage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pre-pull-2", + Namespace: namespace, + }, + Spec: imagesv1.PrePullImageSpec{ + Image: "my-image-2:v0.0.1", + }, + }, metav1.CreateOptions{}) + s.Require().NoError(err) + + result, err := r.Reconcile(context.Background(), ReconcileRequest{ + Name: prePullImageSecond.Name, + Namespace: prePullImageSecond.Namespace, + }) + s.Require().NoError(err) + s.Require().Equal(ControllerResult{}, result) + + // Refresh object, we need to update from latest changes + prePullImageSecond, err = s.imClient.ImagesV1().PrePullImages(prePullImageSecond.Namespace).Get(context.Background(), prePullImageSecond.Name, metav1.GetOptions{}) + s.Require().NoError(err) + + daemonSets, err := s.k8sClient.AppsV1().DaemonSets(prePullImageSecond.Namespace).List(context.Background(), metav1.ListOptions{}) + s.Require().NoError(err) + s.Require().Len(daemonSets.Items, 1, "daemonset should be updated") + s.validateDaemonSetContainsOnlyProperContainers(&daemonSets.Items[0]) + } + + // Mark object for deletion + now := metav1.Now() + prePullImageFirst.DeletionTimestamp = &now + prePullImageFirst, err = s.imClient.ImagesV1().PrePullImages(prePullImageFirst.Namespace).Update(context.Background(), prePullImageFirst, metav1.UpdateOptions{}) + s.Require().NoError(err) + + result, err := r.Reconcile(context.Background(), ReconcileRequest{ + Name: prePullImageFirst.Name, + Namespace: prePullImageFirst.Namespace, + }) + s.Require().NoError(err) + s.Require().Equal(ControllerResult{}, result) + + // Assert updated are commited + daemonSets, err := s.k8sClient.AppsV1().DaemonSets(prePullImageFirst.Namespace).List(context.Background(), metav1.ListOptions{}) + s.Require().NoError(err) + s.Require().Len(daemonSets.Items, 1, "daemonset should be updated") + s.validateDaemonSetContainsOnlyProperContainers(&daemonSets.Items[0]) +}