diff --git a/api/v1beta1/common_consts.go b/api/v1beta1/common_consts.go index 156b1c49..53e1c94b 100644 --- a/api/v1beta1/common_consts.go +++ b/api/v1beta1/common_consts.go @@ -20,9 +20,16 @@ const ( // ElementalManagedLabel label used to put on resources managed by the elemental operator. ElementalManagedLabel = "elemental.cattle.io/managed" - // ElementalManagedLabel label used to put on resources managed by the elemental operator. + // ElementalManagedOSVersionChannelLabel is used to filter a set of ManagedOSVersions given the channel they originate from. ElementalManagedOSVersionChannelLabel = "elemental.cattle.io/channel" + // ElementalManagedOSVersionChannelLastSyncAnnotation reports when a ManagedOSVersion was last synced from a channel. + ElementalManagedOSVersionChannelLastSyncAnnotation = "elemental.cattle.io/channel-last-sync" + + // ElementalManagedOSVersionNoLongerSyncedAnnotation is used to mark a no longer in sync ManagedOSVersion, this highlight it can be deleted. + ElementalManagedOSVersionNoLongerSyncedAnnotation = "elemental.cattle.io/channel-no-longer-in-sync" + ElementalManagedOSVersionNoLongerSyncedValue = "true" + // SASecretSuffix is the suffix used to name registration service account's token secret SASecretSuffix = "-token" diff --git a/controllers/managedosversionchannel_controller.go b/controllers/managedosversionchannel_controller.go index 43eef151..16518223 100644 --- a/controllers/managedosversionchannel_controller.go +++ b/controllers/managedosversionchannel_controller.go @@ -250,7 +250,8 @@ func (r *ManagedOSVersionChannelReconciler) handleSyncPod(ctx context.Context, p if err != nil { return ctrl.Result{}, r.handleFailedSync(ctx, pod, ch, err) } - err = r.createManagedOSVersions(ctx, ch, data) + now := metav1.Now() + err = r.createManagedOSVersions(ctx, ch, data, now.Format(time.RFC3339)) if err != nil { return ctrl.Result{}, r.handleFailedSync(ctx, pod, ch, err) } @@ -265,7 +266,6 @@ func (r *ManagedOSVersionChannelReconciler) handleSyncPod(ctx context.Context, p Message: "successfully loaded channel data", }) ch.Status.FailedSynchronizationAttempts = 0 - now := metav1.Now() ch.Status.LastSyncedTime = &now return ctrl.Result{RequeueAfter: interval}, nil default: @@ -294,7 +294,7 @@ func (r *ManagedOSVersionChannelReconciler) handleFailedSync(ctx context.Context } // createManagedOSVersions unmarshals managedOSVersions from a byte array and creates them. -func (r *ManagedOSVersionChannelReconciler) createManagedOSVersions(ctx context.Context, ch *elementalv1.ManagedOSVersionChannel, data []byte) error { +func (r *ManagedOSVersionChannelReconciler) createManagedOSVersions(ctx context.Context, ch *elementalv1.ManagedOSVersionChannel, data []byte, syncTimestamp string) error { logger := ctrl.LoggerFrom(ctx) vers := []elementalv1.ManagedOSVersion{} @@ -326,6 +326,9 @@ func (r *ManagedOSVersionChannelReconciler) createManagedOSVersions(ctx context. vcpy.ObjectMeta.Labels = map[string]string{ elementalv1.ElementalManagedOSVersionChannelLabel: ch.Name, } + vcpy.ObjectMeta.Annotations = map[string]string{ + elementalv1.ElementalManagedOSVersionChannelLastSyncAnnotation: syncTimestamp, + } if ch.Spec.UpgradeContainer != nil { vcpy.Spec.UpgradeContainer = ch.Spec.UpgradeContainer @@ -334,6 +337,8 @@ func (r *ManagedOSVersionChannelReconciler) createManagedOSVersions(ctx context. if cv, ok := curVersions[v.Name]; ok { patchBase := client.MergeFrom(cv.DeepCopy()) cv.Spec = vcpy.Spec + cv.ObjectMeta.Labels = vcpy.ObjectMeta.Labels + cv.ObjectMeta.Annotations = vcpy.ObjectMeta.Annotations err = r.Patch(ctx, cv, patchBase) if err != nil { logger.Error(err, "failed to patch a managedosversion", "name", cv.Name) @@ -353,7 +358,24 @@ func (r *ManagedOSVersionChannelReconciler) createManagedOSVersions(ctx context. } } - return errorutils.NewAggregate(errs) + if len(errs) > 0 { + return errorutils.NewAggregate(errs) + } + + // Flagging orphan versions + for _, version := range curVersions { + if lastSyncTime, found := version.Annotations[elementalv1.ElementalManagedOSVersionChannelLastSyncAnnotation]; !found || (lastSyncTime != syncTimestamp) { + logger.Info("ManagedOSVersion no longer synced through this channel", "name", version.Name) + patchBase := client.MergeFrom(version.DeepCopy()) + version.ObjectMeta.Annotations[elementalv1.ElementalManagedOSVersionNoLongerSyncedAnnotation] = elementalv1.ElementalManagedOSVersionNoLongerSyncedValue + if err := r.Patch(ctx, version, patchBase); err != nil { + logger.Error(err, "Could not patch ManagedOSVersion as no longer in sync", "name", version.Name) + return fmt.Errorf("deprecating ManagedOSVersion '%s': %w", version.Name, err) + } + } + } + + return nil } // getAllOwnedManagedOSVersions returns a map of all ManagedOSVersions labeled with the given channel, resource name is used as the map key diff --git a/controllers/managedosversionchannel_controller_test.go b/controllers/managedosversionchannel_controller_test.go index 4ef08a8d..450b51a8 100644 --- a/controllers/managedosversionchannel_controller_test.go +++ b/controllers/managedosversionchannel_controller_test.go @@ -77,6 +77,22 @@ const updatedJSON = `[ } ]` +// 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": { @@ -494,7 +510,13 @@ var _ = Describe("managed os version channel controller integration tests", func if mgrCancel != nil { mgrCancel() } - Expect(test.CleanupAndWait(ctx, cl, ch, pod, managedOSVersion)).To(Succeed()) + 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() { @@ -570,6 +592,82 @@ var _ = Describe("managed os version channel controller integration tests", func Expect(managedOSVersion.Spec.Version).To(Equal("v0.1.0-patched")) }) + 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 not reconcile again if it errors during pod lifecycle", func() { ch.Spec.Type = "json"