917 lines
30 KiB
Go
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())
|
|
})
|
|
})
|