elemental-operator/controllers/managedosversionchannel_con...

917 lines
30 KiB
Go

/*
Copyright © 2022 - 2025 SUSE LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controllers
import (
"context"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
elementalv1 "github.com/rancher/elemental-operator/api/v1beta1"
"github.com/rancher/elemental-operator/pkg/test"
ctrlHelpers "github.com/rancher/elemental-operator/tests/controllerHelpers"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
const syncJSON = `[
{
"metadata": {
"name": "v0.1.0"
},
"spec": {
"version": "v0.1.0",
"type": "container",
"metadata": {
"upgradeImage": "foo/bar:v0.1.0"
}
}
}
]`
const updatedJSON = `[
{
"metadata": {
"name": "v0.1.0"
},
"spec": {
"version": "v0.1.0-patched",
"type": "container",
"metadata": {
"upgradeImage": "foo/bar:v0.1.0-patched"
}
}
},
{
"metadata": {
"name": "v0.2.0"
},
"spec": {
"version": "v0.2.0",
"type": "container",
"metadata": {
"upgradeImage": "foo/bar:v0.2.0"
}
}
}
]`
// v0.1.0 removed
const deprecatingJSON = `[
{
"metadata": {
"name": "v0.2.0"
},
"spec": {
"version": "v0.2.0",
"type": "container",
"metadata": {
"upgradeImage": "foo/bar:v0.2.0"
}
}
}
]`
const invalidJSON = `[
{
"metadata": {
"name": "v0.1.0"
},
"spec": {
"version": "v0.1.0",
"type": "container",
"metadata": {
"upgradeImage": "foo/bar:v0.1.0"
}
}
]`
var _ = Describe("reconcile managed os version channel", func() {
var r *ManagedOSVersionChannelReconciler
var managedOSVersionChannel *elementalv1.ManagedOSVersionChannel
var managedOSVersion *elementalv1.ManagedOSVersion
var syncerProvider *ctrlHelpers.FakeSyncerProvider
var pod *corev1.Pod
var setPodPhase func(pod *corev1.Pod, phase corev1.PodPhase)
BeforeEach(func() {
managedOSVersion = &elementalv1.ManagedOSVersion{}
syncerProvider = &ctrlHelpers.FakeSyncerProvider{}
syncerProvider.SetJSON(syncJSON)
r = &ManagedOSVersionChannelReconciler{
Client: cl,
syncerProvider: syncerProvider,
OperatorImage: "test/image:latest",
}
managedOSVersionChannel = &elementalv1.ManagedOSVersionChannel{
ObjectMeta: metav1.ObjectMeta{
Name: "test-name",
Namespace: "default",
},
Spec: elementalv1.ManagedOSVersionChannelSpec{
Enabled: true,
},
}
pod = &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "test-name",
Namespace: "default",
},
}
setPodPhase = func(pod *corev1.Pod, phase corev1.PodPhase) {
patchBase := client.MergeFrom(pod.DeepCopy())
pod.Status.Phase = phase
Expect(cl.Status().Patch(ctx, pod, patchBase)).To(Succeed())
}
})
AfterEach(func() {
Expect(test.CleanupAndWait(ctx, cl, managedOSVersionChannel, pod, managedOSVersion)).To(Succeed())
})
It("should reconcile and sync managed os version channel object", func() {
managedOSVersionChannel.Spec.Type = "json"
managedOSVersionChannel.Spec.SyncInterval = "1m"
name := types.NamespacedName{
Namespace: managedOSVersionChannel.Namespace,
Name: managedOSVersionChannel.Name,
}
Expect(cl.Create(ctx, managedOSVersionChannel)).To(Succeed())
// No error and status updated (no requeue)
_, err := r.Reconcile(ctx, reconcile.Request{NamespacedName: name})
Expect(err).ToNot(HaveOccurred())
Expect(cl.Get(ctx, client.ObjectKey{
Name: managedOSVersionChannel.Name,
Namespace: managedOSVersionChannel.Namespace,
}, managedOSVersionChannel)).To(Succeed())
Expect(managedOSVersionChannel.Status.Conditions[0].Reason).To(Equal(elementalv1.SyncingReason))
Expect(managedOSVersionChannel.Status.Conditions[0].Status).To(Equal(metav1.ConditionFalse))
Expect(cl.Get(ctx, client.ObjectKey{
Name: pod.Name,
Namespace: pod.Namespace,
}, pod)).To(Succeed())
Expect(pod.Status.Phase).To(Equal(corev1.PodPending))
res, err := r.Reconcile(ctx, reconcile.Request{NamespacedName: name})
Expect(err).ToNot(HaveOccurred())
setPodPhase(pod, corev1.PodRunning)
res, err = r.Reconcile(ctx, reconcile.Request{NamespacedName: name})
Expect(err).ToNot(HaveOccurred())
setPodPhase(pod, corev1.PodSucceeded)
res, err = r.Reconcile(ctx, reconcile.Request{NamespacedName: name})
Expect(err).ToNot(HaveOccurred())
Expect(res.RequeueAfter).To(Equal(60 * time.Second))
Expect(cl.Get(ctx, client.ObjectKey{
Name: managedOSVersionChannel.Name,
Namespace: managedOSVersionChannel.Namespace,
}, managedOSVersionChannel)).To(Succeed())
Expect(managedOSVersionChannel.Status.Conditions[0].Reason).To(Equal(elementalv1.SyncedReason))
Expect(managedOSVersionChannel.Status.Conditions[0].Status).To(Equal(metav1.ConditionTrue))
// Now the managed os vesion is already created
Expect(cl.Get(ctx, client.ObjectKey{
Name: "v0.1.0",
Namespace: managedOSVersionChannel.Namespace,
}, managedOSVersion)).To(Succeed())
// Synchronization done, pod deleted
Expect(cl.Get(ctx, client.ObjectKey{
Name: pod.Name,
Namespace: pod.Namespace,
}, pod)).NotTo(Succeed())
// Re-sync is triggered to interval
res, err = r.Reconcile(ctx, reconcile.Request{NamespacedName: name})
Expect(err).ToNot(HaveOccurred())
Expect(res.RequeueAfter).To(BeNumerically("~", 59*time.Second, 1*time.Minute))
})
It("should reconcile managed os version channel object without a type", func() {
managedOSVersionChannel.Spec.SyncInterval = "1m"
Expect(cl.Create(ctx, managedOSVersionChannel)).To(Succeed())
res, err := r.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: managedOSVersionChannel.Namespace,
Name: managedOSVersionChannel.Name,
},
})
Expect(err).ToNot(HaveOccurred())
Expect(res.RequeueAfter).To(Equal(0 * time.Second))
Expect(res.Requeue).To(BeFalse())
Expect(cl.Get(ctx, client.ObjectKey{
Name: managedOSVersionChannel.Name,
Namespace: managedOSVersionChannel.Namespace,
}, managedOSVersionChannel)).To(Succeed())
Expect(managedOSVersionChannel.Status.Conditions[0].Reason).To(Equal(elementalv1.InvalidConfigurationReason))
Expect(managedOSVersionChannel.Status.Conditions[0].Status).To(Equal(metav1.ConditionFalse))
})
It("should reconcile managed os version channel object with a bad a sync interval", func() {
managedOSVersionChannel.Spec.Type = "custom"
managedOSVersionChannel.Spec.SyncInterval = "badtime"
Expect(cl.Create(ctx, managedOSVersionChannel)).To(Succeed())
res, err := r.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: managedOSVersionChannel.Namespace,
Name: managedOSVersionChannel.Name,
},
})
Expect(err).ToNot(HaveOccurred())
Expect(res.RequeueAfter).To(Equal(0 * time.Second))
Expect(res.Requeue).To(BeFalse())
Expect(cl.Get(ctx, client.ObjectKey{
Name: managedOSVersionChannel.Name,
Namespace: managedOSVersionChannel.Namespace,
}, managedOSVersionChannel)).To(Succeed())
Expect(managedOSVersionChannel.Status.Conditions[0].Reason).To(Equal(elementalv1.InvalidConfigurationReason))
Expect(managedOSVersionChannel.Status.Conditions[0].Status).To(Equal(metav1.ConditionFalse))
})
It("should reconcile managed os version channel object with a not valid type", func() {
syncerProvider.UnknownType = "unknown"
managedOSVersionChannel.Spec.Type = "unknown"
managedOSVersionChannel.Spec.SyncInterval = "1m"
Expect(cl.Create(ctx, managedOSVersionChannel)).To(Succeed())
res, err := r.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: managedOSVersionChannel.Namespace,
Name: managedOSVersionChannel.Name,
},
})
Expect(err).ToNot(HaveOccurred())
Expect(res.RequeueAfter).To(Equal(0 * time.Second))
Expect(cl.Get(ctx, client.ObjectKey{
Name: managedOSVersionChannel.Name,
Namespace: managedOSVersionChannel.Namespace,
}, managedOSVersionChannel)).To(Succeed())
Expect(managedOSVersionChannel.Status.Conditions[0].Reason).To(Equal(elementalv1.InvalidConfigurationReason))
Expect(managedOSVersionChannel.Status.Conditions[0].Status).To(Equal(metav1.ConditionFalse))
})
It("it fails to reconcile a managed os version channel when channel provides invalid JSON", func() {
syncerProvider.SetJSON(invalidJSON)
managedOSVersionChannel.Spec.Type = "json"
name := types.NamespacedName{
Namespace: managedOSVersionChannel.Namespace,
Name: managedOSVersionChannel.Name,
}
Expect(cl.Create(ctx, managedOSVersionChannel)).To(Succeed())
// No error and status updated (no requeue)
res, err := r.Reconcile(ctx, reconcile.Request{NamespacedName: name})
Expect(err).ToNot(HaveOccurred())
Expect(res.RequeueAfter).To(Equal(0 * time.Second))
Expect(cl.Get(ctx, client.ObjectKey{
Name: managedOSVersionChannel.Name,
Namespace: managedOSVersionChannel.Namespace,
}, managedOSVersionChannel)).To(Succeed())
Expect(managedOSVersionChannel.Status.Conditions[0].Reason).To(Equal(elementalv1.SyncingReason))
Expect(managedOSVersionChannel.Status.Conditions[0].Status).To(Equal(metav1.ConditionFalse))
Expect(cl.Get(ctx, client.ObjectKey{
Name: pod.Name,
Namespace: pod.Namespace,
}, pod)).To(Succeed())
setPodPhase(pod, corev1.PodSucceeded)
_, err = r.Reconcile(ctx, reconcile.Request{NamespacedName: name})
Expect(err).To(HaveOccurred())
Expect(cl.Get(ctx, client.ObjectKey{
Name: managedOSVersionChannel.Name,
Namespace: managedOSVersionChannel.Namespace,
}, managedOSVersionChannel)).To(Succeed())
Expect(managedOSVersionChannel.Status.Conditions[0].Reason).To(Equal(elementalv1.FailedToSyncReason))
Expect(managedOSVersionChannel.Status.Conditions[0].Status).To(Equal(metav1.ConditionFalse))
// Synchronization failed, pod deleted
Expect(cl.Get(ctx, client.ObjectKey{
Name: pod.Name,
Namespace: pod.Namespace,
}, pod)).NotTo(Succeed())
})
It("it fails to reconcile when logs can't be read", func() {
syncerProvider.LogsError = true
managedOSVersionChannel.Spec.Type = "json"
name := types.NamespacedName{
Namespace: managedOSVersionChannel.Namespace,
Name: managedOSVersionChannel.Name,
}
Expect(cl.Create(ctx, managedOSVersionChannel)).To(Succeed())
// No error and status updated (no requeue)
_, err := r.Reconcile(ctx, reconcile.Request{NamespacedName: name})
Expect(err).ToNot(HaveOccurred())
Expect(cl.Get(ctx, client.ObjectKey{
Name: managedOSVersionChannel.Name,
Namespace: managedOSVersionChannel.Namespace,
}, managedOSVersionChannel)).To(Succeed())
Expect(managedOSVersionChannel.Status.Conditions[0].Reason).To(Equal(elementalv1.SyncingReason))
Expect(managedOSVersionChannel.Status.Conditions[0].Status).To(Equal(metav1.ConditionFalse))
Expect(cl.Get(ctx, client.ObjectKey{
Name: pod.Name,
Namespace: pod.Namespace,
}, pod)).To(Succeed())
Expect(pod.Status.Phase).To(Equal(corev1.PodPending))
setPodPhase(pod, corev1.PodSucceeded)
_, err = r.Reconcile(ctx, reconcile.Request{NamespacedName: name})
Expect(err).To(HaveOccurred())
Expect(cl.Get(ctx, client.ObjectKey{
Name: managedOSVersionChannel.Name,
Namespace: managedOSVersionChannel.Namespace,
}, managedOSVersionChannel)).To(Succeed())
Expect(managedOSVersionChannel.Status.Conditions[0].Reason).To(Equal(elementalv1.FailedToSyncReason))
Expect(managedOSVersionChannel.Status.Conditions[0].Status).To(Equal(metav1.ConditionFalse))
// Synchronization failed, pod deleted
Expect(cl.Get(ctx, client.ObjectKey{
Name: pod.Name,
Namespace: pod.Namespace,
}, pod)).NotTo(Succeed())
})
It("it fails to reconcile if syncer pod process fails", func() {
managedOSVersionChannel.Spec.Type = "json"
name := types.NamespacedName{
Namespace: managedOSVersionChannel.Namespace,
Name: managedOSVersionChannel.Name,
}
Expect(cl.Create(ctx, managedOSVersionChannel)).To(Succeed())
// No error and status updated (no requeue)
_, err := r.Reconcile(ctx, reconcile.Request{NamespacedName: name})
Expect(err).ToNot(HaveOccurred())
Expect(cl.Get(ctx, client.ObjectKey{
Name: managedOSVersionChannel.Name,
Namespace: managedOSVersionChannel.Namespace,
}, managedOSVersionChannel)).To(Succeed())
Expect(managedOSVersionChannel.Status.Conditions[0].Reason).To(Equal(elementalv1.SyncingReason))
Expect(managedOSVersionChannel.Status.Conditions[0].Status).To(Equal(metav1.ConditionFalse))
Expect(cl.Get(ctx, client.ObjectKey{
Name: pod.Name,
Namespace: pod.Namespace,
}, pod)).To(Succeed())
Expect(pod.Status.Phase).To(Equal(corev1.PodPending))
setPodPhase(pod, corev1.PodFailed)
_, err = r.Reconcile(ctx, reconcile.Request{NamespacedName: name})
Expect(err).To(HaveOccurred())
Expect(cl.Get(ctx, client.ObjectKey{
Name: managedOSVersionChannel.Name,
Namespace: managedOSVersionChannel.Namespace,
}, managedOSVersionChannel)).To(Succeed())
Expect(managedOSVersionChannel.Status.Conditions[0].Reason).To(Equal(elementalv1.FailedToSyncReason))
Expect(managedOSVersionChannel.Status.Conditions[0].Status).To(Equal(metav1.ConditionFalse))
// Synchronization failed, pod deleted
Expect(cl.Get(ctx, client.ObjectKey{
Name: pod.Name,
Namespace: pod.Namespace,
}, pod)).NotTo(Succeed())
})
It("it fails to reconcile if operator image is undefined", func() {
r.OperatorImage = ""
managedOSVersionChannel.Spec.Type = "json"
name := types.NamespacedName{
Namespace: managedOSVersionChannel.Namespace,
Name: managedOSVersionChannel.Name,
}
Expect(cl.Create(ctx, managedOSVersionChannel)).To(Succeed())
// No error and status updated (no requeue)
_, err := r.Reconcile(ctx, reconcile.Request{NamespacedName: name})
Expect(err).To(HaveOccurred())
Expect(cl.Get(ctx, client.ObjectKey{
Name: managedOSVersionChannel.Name,
Namespace: managedOSVersionChannel.Namespace,
}, managedOSVersionChannel)).To(Succeed())
Expect(managedOSVersionChannel.Status.Conditions[0].Reason).To(Equal(elementalv1.FailedToCreatePodReason))
Expect(managedOSVersionChannel.Status.Conditions[0].Status).To(Equal(metav1.ConditionFalse))
})
It("should deprecate ManagedOSVersions when disabled", func() {
// Pre-populate synced ManagedOSVersion
managedOSVersion.Name = "test-disabled"
managedOSVersion.Namespace = managedOSVersionChannel.Namespace
managedOSVersion.Labels = map[string]string{elementalv1.ElementalManagedOSVersionChannelLabel: managedOSVersionChannel.Name}
Expect(cl.Create(ctx, managedOSVersion)).Should(Succeed())
// Create channel
managedOSVersionChannel.Spec.Type = "json"
managedOSVersionChannel.Spec.SyncInterval = "1m"
managedOSVersionChannel.Spec.Enabled = false
name := types.NamespacedName{
Namespace: managedOSVersionChannel.Namespace,
Name: managedOSVersionChannel.Name,
}
Expect(cl.Create(ctx, managedOSVersionChannel)).To(Succeed())
// No error and status updated (no requeue)
_, err := r.Reconcile(ctx, reconcile.Request{NamespacedName: name})
Expect(err).ToNot(HaveOccurred())
Expect(cl.Get(ctx, types.NamespacedName{
Name: managedOSVersion.Name,
Namespace: managedOSVersion.Namespace,
}, managedOSVersion))
noLongerInSync, found := managedOSVersion.ObjectMeta.Annotations[elementalv1.ElementalManagedOSVersionNoLongerSyncedAnnotation]
Expect(found).Should(BeTrue(), "ElementalManagedOSVersionNoLongerSyncedAnnotation must be present")
Expect(noLongerInSync).Should(Equal(elementalv1.ElementalManagedOSVersionNoLongerSyncedValue), "ManagedOSVersion must be marked as no longer in sync")
})
It("should delete ManagedOSVersions when disabled", func() {
// Pre-populate synced ManagedOSVersion
managedOSVersion.Name = "test-disabled"
managedOSVersion.Namespace = managedOSVersionChannel.Namespace
managedOSVersion.Labels = map[string]string{elementalv1.ElementalManagedOSVersionChannelLabel: managedOSVersionChannel.Name}
Expect(cl.Create(ctx, managedOSVersion)).Should(Succeed())
// Create channel
managedOSVersionChannel.Spec.Type = "json"
managedOSVersionChannel.Spec.SyncInterval = "1m"
managedOSVersionChannel.Spec.Enabled = false
managedOSVersionChannel.Spec.DeleteNoLongerInSyncVersions = true
name := types.NamespacedName{
Namespace: managedOSVersionChannel.Namespace,
Name: managedOSVersionChannel.Name,
}
Expect(cl.Create(ctx, managedOSVersionChannel)).To(Succeed())
// No error and status updated (no requeue)
_, err := r.Reconcile(ctx, reconcile.Request{NamespacedName: name})
Expect(err).ToNot(HaveOccurred())
Eventually(func() bool {
err := cl.Get(ctx, types.NamespacedName{
Name: managedOSVersion.Name,
Namespace: managedOSVersion.Namespace,
}, managedOSVersion)
return apierrors.IsNotFound(err)
}, time.Minute).Should(BeTrue(), "ManagedOSVersion must have been deleted")
})
It("should delete syncer pod when disabled", func() {
// Pre-populate syncer pod
pod.Spec.Containers = []corev1.Container{
{
Name: "test",
Image: r.OperatorImage,
},
}
Expect(cl.Create(ctx, pod)).Should(Succeed())
// Create channel
managedOSVersionChannel.Spec.Type = "json"
managedOSVersionChannel.Spec.SyncInterval = "1m"
managedOSVersionChannel.Spec.Enabled = false
name := types.NamespacedName{
Namespace: managedOSVersionChannel.Namespace,
Name: managedOSVersionChannel.Name,
}
Expect(cl.Create(ctx, managedOSVersionChannel)).To(Succeed())
// No error and status updated (no requeue)
_, err := r.Reconcile(ctx, reconcile.Request{NamespacedName: name})
Expect(err).ToNot(HaveOccurred())
Eventually(func() bool {
err := cl.Get(ctx, types.NamespacedName{
Name: pod.Name,
Namespace: pod.Namespace,
}, pod)
return apierrors.IsNotFound(err)
}, time.Minute).Should(BeTrue(), "syncer pod must have been deleted")
})
})
var _ = Describe("managed os version channel controller integration tests", func() {
var r *ManagedOSVersionChannelReconciler
var ch *elementalv1.ManagedOSVersionChannel
var managedOSVersion *elementalv1.ManagedOSVersion
var syncerProvider *ctrlHelpers.FakeSyncerProvider
var mgr manager.Manager
var mgrCtx context.Context
var mgrCancel context.CancelFunc
var pod *corev1.Pod
var setPodPhase func(pod *corev1.Pod, phase corev1.PodPhase)
BeforeEach(func() {
var err error
managedOSVersion = &elementalv1.ManagedOSVersion{}
mgr, err = ctrl.NewManager(cfg, ctrl.Options{
Scheme: cl.Scheme(),
})
Expect(err).ToNot(HaveOccurred())
syncerProvider = &ctrlHelpers.FakeSyncerProvider{}
syncerProvider.SetJSON(syncJSON)
r = &ManagedOSVersionChannelReconciler{
Client: cl,
syncerProvider: syncerProvider,
OperatorImage: "test/image:latest",
}
Expect(r.SetupWithManager(mgr)).To(Succeed())
ch = &elementalv1.ManagedOSVersionChannel{
ObjectMeta: metav1.ObjectMeta{
Name: "test-name",
Namespace: "default",
},
Spec: elementalv1.ManagedOSVersionChannelSpec{
Enabled: true,
},
}
pod = &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "test-name",
Namespace: "default",
},
}
setPodPhase = func(pod *corev1.Pod, phase corev1.PodPhase) {
patchBase := client.MergeFrom(pod.DeepCopy())
pod.Status.Phase = phase
Expect(cl.Status().Patch(ctx, pod, patchBase)).To(Succeed())
}
mgrCtx, mgrCancel = context.WithCancel(ctx)
go func() {
defer GinkgoRecover()
err = mgr.Start(mgrCtx)
Expect(err).ToNot(HaveOccurred(), "failed to run manager")
}()
})
AfterEach(func() {
if mgrCancel != nil {
mgrCancel()
}
Expect(test.CleanupAndWait(ctx, cl, ch, pod)).To(Succeed())
list := &elementalv1.ManagedOSVersionList{}
Expect(cl.List(ctx, list)).To(Succeed())
for _, version := range list.Items {
Expect(test.CleanupAndWait(ctx, cl, &version)).To(Succeed())
}
})
It("should reconcile and sync managed os version channel object and apply channel updates", func() {
ch.Spec.Type = "json"
Expect(cl.Create(ctx, ch)).To(Succeed())
// Pod is created
Eventually(func() bool {
err := cl.Get(ctx, client.ObjectKey{
Name: ch.Name,
Namespace: ch.Namespace,
}, pod)
return err == nil
}, 12*time.Second, 2*time.Second).Should(BeTrue())
setPodPhase(pod, corev1.PodSucceeded)
Eventually(func() bool {
err := cl.Get(ctx, client.ObjectKey{
Name: ch.Name,
Namespace: ch.Namespace,
}, ch)
return err == nil && ch.Status.Conditions[0].Status == metav1.ConditionTrue
}, 12*time.Second, 2*time.Second).Should(BeTrue())
Expect(cl.Get(ctx, client.ObjectKey{
Name: "v0.1.0",
Namespace: ch.Namespace,
}, managedOSVersion)).To(Succeed())
Expect(managedOSVersion.Spec.Version).To(Equal("v0.1.0"))
// Pod is deleted
Eventually(func() bool {
err := cl.Get(ctx, client.ObjectKey{
Name: ch.Name,
Namespace: ch.Namespace,
}, pod)
return err != nil && apierrors.IsNotFound(err)
}, 6*time.Second, 1*time.Second).Should(BeTrue())
// Simulate a channel content change
syncerProvider.SetJSON(updatedJSON)
// Updating the channel causes an automatic update
patchBase := client.MergeFrom(ch.DeepCopy())
ch.Spec.SyncInterval = "10s"
Expect(cl.Patch(ctx, ch, patchBase)).To(Succeed())
// Pod is created
Eventually(func() bool {
err := cl.Get(ctx, client.ObjectKey{
Name: ch.Name,
Namespace: ch.Namespace,
}, pod)
return err == nil
}, 6*time.Second, 1*time.Second).Should(BeTrue())
setPodPhase(pod, corev1.PodSucceeded)
// New added versions are synced
Eventually(func() bool {
err := cl.Get(ctx, client.ObjectKey{
Name: "v0.2.0",
Namespace: ch.Namespace,
}, managedOSVersion)
return err == nil
}, 6*time.Second, 1*time.Second).Should(BeTrue())
// After channel update already existing versions were patched
Expect(cl.Get(ctx, client.ObjectKey{
Name: "v0.1.0",
Namespace: ch.Namespace,
}, managedOSVersion)).To(Succeed())
Expect(managedOSVersion.Spec.Version).To(Equal("v0.1.0-patched"))
// Pod is deleted
Eventually(func() bool {
err := cl.Get(ctx, client.ObjectKey{
Name: ch.Name,
Namespace: ch.Namespace,
}, pod)
return err != nil && apierrors.IsNotFound(err)
}, 2*time.Second, 1*time.Second).Should(BeTrue())
Expect(cl.Get(ctx, client.ObjectKey{
Name: ch.Name,
Namespace: ch.Namespace,
}, ch)).To(Succeed())
// Simulate another channel content change
syncerProvider.SetJSON(deprecatingJSON)
timeout := time.Until(ch.Status.LastSyncedTime.Add(10*time.Second)) - 1*time.Second
// No pod is created during the interval
Consistently(func() bool {
err := cl.Get(ctx, client.ObjectKey{
Name: ch.Name,
Namespace: ch.Namespace,
}, pod)
return apierrors.IsNotFound(err)
}, timeout, 1*time.Second).Should(BeTrue())
// Pod is created once the resync is triggered automatically
Eventually(func() bool {
err := cl.Get(ctx, client.ObjectKey{
Name: ch.Name,
Namespace: ch.Namespace,
}, pod)
return err == nil
}, 4*time.Second, 1*time.Second).Should(BeTrue())
setPodPhase(pod, corev1.PodSucceeded)
// v0.1.0 becomes a managedOSVersion out of sync
Eventually(func() bool {
Expect(cl.Get(ctx, client.ObjectKey{
Name: "v0.1.0",
Namespace: ch.Namespace,
}, managedOSVersion)).To(Succeed())
return managedOSVersion.Annotations[elementalv1.ElementalManagedOSVersionNoLongerSyncedAnnotation] == elementalv1.ElementalManagedOSVersionNoLongerSyncedValue
}, 6*time.Second, 1*time.Second).Should(BeTrue())
})
It("should deprecate a version after it's removed from channel", func() {
ch.Spec.Type = "json"
Expect(cl.Create(ctx, ch)).To(Succeed())
// Pod is created
Eventually(func() bool {
err := cl.Get(ctx, client.ObjectKey{
Name: ch.Name,
Namespace: ch.Namespace,
}, pod)
return err == nil
}, 12*time.Second, 2*time.Second).Should(BeTrue())
setPodPhase(pod, corev1.PodSucceeded)
Eventually(func() bool {
err := cl.Get(ctx, client.ObjectKey{
Name: ch.Name,
Namespace: ch.Namespace,
}, ch)
return err == nil && ch.Status.Conditions[0].Status == metav1.ConditionTrue
}, 12*time.Second, 2*time.Second).Should(BeTrue())
Expect(cl.Get(ctx, client.ObjectKey{
Name: "v0.1.0",
Namespace: ch.Namespace,
}, managedOSVersion)).To(Succeed())
Expect(managedOSVersion.Spec.Version).To(Equal("v0.1.0"))
// Pod is deleted
Eventually(func() bool {
err := cl.Get(ctx, client.ObjectKey{
Name: ch.Name,
Namespace: ch.Namespace,
}, pod)
return err != nil && apierrors.IsNotFound(err)
}, 12*time.Second, 2*time.Second).Should(BeTrue())
// Simulate a channel content change
syncerProvider.SetJSON(deprecatingJSON)
// Updating the channel after the minimum time between syncs causes an automatic update
patchBase := client.MergeFrom(ch.DeepCopy())
ch.Spec.SyncInterval = "10m"
Expect(cl.Patch(ctx, ch, patchBase)).To(Succeed())
// Pod is created
Eventually(func() bool {
err := cl.Get(ctx, client.ObjectKey{
Name: ch.Name,
Namespace: ch.Namespace,
}, pod)
return err == nil
}, 12*time.Second, 2*time.Second).Should(BeTrue())
setPodPhase(pod, corev1.PodSucceeded)
// New added versions are synced
Eventually(func() bool {
err := cl.Get(ctx, client.ObjectKey{
Name: "v0.2.0",
Namespace: ch.Namespace,
}, managedOSVersion)
return err == nil
}, 12*time.Second, 2*time.Second).Should(BeTrue())
_, found := managedOSVersion.Annotations[elementalv1.ElementalManagedOSVersionNoLongerSyncedAnnotation]
Expect(found).To(BeFalse(), "no-longer-synced annotation must not be present when versions are actually synced")
Expect(managedOSVersion.Annotations[elementalv1.ElementalManagedOSVersionChannelLastSyncAnnotation]).ToNot(BeEmpty(), "Last sync annotation should contain the UTC timestamp")
// After channel update already existing versions were patched
Expect(cl.Get(ctx, client.ObjectKey{
Name: "v0.1.0",
Namespace: ch.Namespace,
}, managedOSVersion)).To(Succeed())
Expect(managedOSVersion.Annotations[elementalv1.ElementalManagedOSVersionNoLongerSyncedAnnotation]).To(Equal(elementalv1.ElementalManagedOSVersionNoLongerSyncedValue))
})
It("should auto-delete a version after it's removed from channel", func() {
ch.Spec.Type = "json"
ch.Spec.DeleteNoLongerInSyncVersions = true
Expect(cl.Create(ctx, ch)).To(Succeed())
// Pod is created
Eventually(func() bool {
err := cl.Get(ctx, client.ObjectKey{
Name: ch.Name,
Namespace: ch.Namespace,
}, pod)
return err == nil
}, 12*time.Second, 2*time.Second).Should(BeTrue())
setPodPhase(pod, corev1.PodSucceeded)
Eventually(func() bool {
err := cl.Get(ctx, client.ObjectKey{
Name: ch.Name,
Namespace: ch.Namespace,
}, ch)
return err == nil && ch.Status.Conditions[0].Status == metav1.ConditionTrue
}, 12*time.Second, 2*time.Second).Should(BeTrue())
Expect(cl.Get(ctx, client.ObjectKey{
Name: "v0.1.0",
Namespace: ch.Namespace,
}, managedOSVersion)).To(Succeed())
Expect(managedOSVersion.Spec.Version).To(Equal("v0.1.0"))
// Pod is deleted
Eventually(func() bool {
err := cl.Get(ctx, client.ObjectKey{
Name: ch.Name,
Namespace: ch.Namespace,
}, pod)
return err != nil && apierrors.IsNotFound(err)
}, 12*time.Second, 2*time.Second).Should(BeTrue())
// Simulate a channel content change
syncerProvider.SetJSON(deprecatingJSON)
// Updating the channel after the minimum time between syncs causes an automatic update
patchBase := client.MergeFrom(ch.DeepCopy())
ch.Spec.SyncInterval = "10m"
Expect(cl.Patch(ctx, ch, patchBase)).To(Succeed())
// Pod is created
Eventually(func() bool {
err := cl.Get(ctx, client.ObjectKey{
Name: ch.Name,
Namespace: ch.Namespace,
}, pod)
return err == nil
}, 12*time.Second, 2*time.Second).Should(BeTrue())
setPodPhase(pod, corev1.PodSucceeded)
// Check deprecated version was deleted
Eventually(func() bool {
err := cl.Get(ctx, client.ObjectKey{
Name: "v0.1.0",
Namespace: ch.Namespace,
}, managedOSVersion)
return apierrors.IsNotFound(err)
}, 12*time.Second, 2*time.Second).Should(BeTrue(), "No longer in sync version should have been deleted")
})
It("should not reconcile again if it errors during pod lifecycle", func() {
ch.Spec.Type = "json"
Expect(cl.Create(ctx, ch)).To(Succeed())
// Pod is created
Eventually(func() bool {
err := cl.Get(ctx, client.ObjectKey{
Name: ch.Name,
Namespace: ch.Namespace,
}, pod)
return err == nil
}, 4*time.Second, 1*time.Second).Should(BeTrue())
setPodPhase(pod, corev1.PodFailed)
Eventually(func() bool {
err := cl.Get(ctx, client.ObjectKey{
Name: ch.Name,
Namespace: ch.Namespace,
}, ch)
return err == nil && ch.Status.Conditions[0].Reason == elementalv1.FailedToSyncReason
}, 12*time.Second, 2*time.Second).Should(BeTrue())
// Pod is deleted
Eventually(func() bool {
err := cl.Get(ctx, client.ObjectKey{
Name: ch.Name,
Namespace: ch.Namespace,
}, pod)
return err != nil && apierrors.IsNotFound(err)
}, 12*time.Second, 2*time.Second).Should(BeTrue())
})
})