diff --git a/.obs/chartfile/crds/templates/crds.yaml b/.obs/chartfile/crds/templates/crds.yaml index 4bdee9e65..0df1f1df0 100644 --- a/.obs/chartfile/crds/templates/crds.yaml +++ b/.obs/chartfile/crds/templates/crds.yaml @@ -2570,6 +2570,9 @@ spec: DeleteNoLongerInSyncVersions automatically deletes all no-longer-in-sync ManagedOSVersions that were created by this channel. type: boolean + enabled: + default: true + type: boolean options: x-kubernetes-preserve-unknown-fields: true syncInterval: diff --git a/.obs/chartfile/operator/questions.yaml b/.obs/chartfile/operator/questions.yaml index f8e3a495f..75e7bcf94 100644 --- a/.obs/chartfile/operator/questions.yaml +++ b/.obs/chartfile/operator/questions.yaml @@ -1,24 +1,43 @@ questions: +- variable: defaultChannels.sleMicro55.included + show_if: "defaultChannels.sleMicro55" + default: true + description: "Default channel that can be used for any generic workload." + type: boolean + label: SLE Micro 5.5 + group: "Default Elemental OS Channels" +- variable: defaultChannels.sleMicro55KVM.included + show_if: "defaultChannels.sleMicro55KVM" + default: true + description: "Ready to be used with KVM. Contains QEMU Guest agent by default." + type: boolean + label: SLE Micro 5.5 KVM + group: "Default Elemental OS Channels" +- variable: defaultChannels.sleMicro55RT.included + show_if: "defaultChannels.sleMicro55RT" + default: true + description: "Channel that can be used for any generic workload with a Real-Time kernel." + type: boolean + label: SLE Micro 5.5 RT + group: "Default Elemental OS Channels" - variable: channel.defaultChannel - default: "true" - description: "Provide an Elemental OS Channel container image" - label: Elemental OS Channel + default: "false" + description: "Provide a Custom OS Channel container image" + label: Custom OS Channel type: boolean show_subquestion_if: true - group: "Elemental OS Channel" + group: "Custom OS Channel" subquestions: - variable: channel.image - default: "%%IMG_REPO%%/rancher/elemental-channel" - description: "Specify the Elemental OS channel: for air-gapped scenarios you need to provide your own OS channel image (see https://elemental.docs.rancher.com/airgap for detailed instructions)" + description: "Specify the custom OS channel: for air-gapped scenarios please see https://elemental.docs.rancher.com/airgap" type: string - label: Elemental OS Channel Image - group: "Elemental OS Channel" + label: Custom OS Channel Image + group: "Custom OS Channel" - variable: channel.tag - default: "%VERSION%" - description: "Specify Elemental OS channel image tag" + description: "Specify Custom OS Channel image tag" type: string - label: "Elemental OS Channel Tag" - group: "Elemental OS Channel" + label: "Custom OS Channel Tag" + group: "Custom OS Channel" - variable: debug default: "false" description: "Enable debug logging in the Elemental operator" diff --git a/.obs/chartfile/operator/templates/channels.yaml b/.obs/chartfile/operator/templates/channels.yaml index c180d36d0..c4e22cbc7 100644 --- a/.obs/chartfile/operator/templates/channels.yaml +++ b/.obs/chartfile/operator/templates/channels.yaml @@ -12,6 +12,24 @@ spec: type: custom {{ end }} +{{ range $key, $channel := .Values.defaultChannels }} + {{ if and $channel.included (not (lookup "elemental.cattle.io/v1beta1" "ManagedOSVersionChannel" "fleet-default" "$channel.name")) }} +--- +apiVersion: elemental.cattle.io/v1beta1 +kind: ManagedOSVersionChannel +metadata: + name: {{ $channel.name }} + namespace: fleet-default +spec: + deleteNoLongerInSyncVersions: {{ $channel.deleteNoLongerInSyncVersions }} + enabled: {{ $channel.enabled }} + options: + image: {{ $channel.image }} + type: custom + {{ end }} +{{ end }} + + # Keep pre-existing channels managed by Helm if they do not match with the current default # this way if an upgrade introduces a new channel any pre-existing channel managed by Helm is not deleted {{ range $index, $channel := (lookup "elemental.cattle.io/v1beta1" "ManagedOSVersionChannel" "fleet-default" "").items }} @@ -23,8 +41,6 @@ metadata: name: {{ $channel.metadata.name }} namespace: fleet-default spec: - options: - image: {{ $channel.spec.options.image }} - type: custom + {{- toYaml $channel.spec | nindent 2}} {{ end }} {{ end }} diff --git a/.obs/chartfile/operator/values.yaml b/.obs/chartfile/operator/values.yaml index 8abdce967..9cd7c4db0 100644 --- a/.obs/chartfile/operator/values.yaml +++ b/.obs/chartfile/operator/values.yaml @@ -9,10 +9,32 @@ seedImage: tag: "%VERSION%" imagePullPolicy: IfNotPresent -channel: - name: "sle-micro-5.5" - image: "%%IMG_REPO%%/rancher/elemental-channel/sle-micro" - tag: "5.5" +# a custom channel to install +#channel: +# name: "my-os-channel" +# image: "my-repo/my-os-channel" +# tag: "1.2.3" + +# default Elemental channels +defaultChannels: + sleMicro55: + included: true + name: sle-micro-5.5 + enabled: false + image: registry.opensuse.org/isv/rancher/elemental/dev/containers/rancher/elemental-channel/sle-micro:5.5 + deleteNoLongerInSyncVersions: true + sleMicro55KVM: + included: true + name: sle-micro-5.5-kvm + enabled: false + image: registry.opensuse.org/isv/rancher/elemental/dev/containers/rancher/elemental-channel/sle-micro:5.5-kvm + deleteNoLongerInSyncVersions: true + sleMicro55RT: + included: true + name: sle-micro-5.5-rt + enabled: false + image: registry.opensuse.org/isv/rancher/elemental/dev/containers/rancher/elemental-channel/sle-micro:5.5-rt + deleteNoLongerInSyncVersions: true # number of operator replicas to deploy replicas: 1 diff --git a/Makefile b/Makefile index 4859207ec..34d07a927 100644 --- a/Makefile +++ b/Makefile @@ -193,13 +193,14 @@ setup-full-cluster: build-docker-operator build-docker-seedimage-builder chart s kind load docker-image --name $(CLUSTER_NAME) ${REGISTRY_HEADER}${REPO_SEEDIMAGE}:${TAG_SEEDIMAGE} && \ cd $(ROOT_DIR)/tests && $(GINKGO) -r -v --label-filter="do-nothing" ./e2e -# This builds the docker image, generates the chart, loads the image into the kind cluster and upgrades the chart to latest +# This generates the chart, builds the docker image, loads the image into the kind cluster and upgrades the chart to latest # useful to test changes into the operator with a running system, without clearing the operator namespace # thus losing any registration/inventories/os CRDs already created -reload-operator: build-docker-operator chart +reload-operator: chart build-docker-operator kind load docker-image --name $(CLUSTER_NAME) ${REGISTRY_HEADER}${REPO}:${CHART_VERSION} helm upgrade -n cattle-elemental-system elemental-operator-crds $(CHART_CRDS) helm upgrade -n cattle-elemental-system elemental-operator $(CHART) + kubectl -n cattle-elemental-system rollout restart deployment/elemental-operator .PHONY: vendor vendor: diff --git a/api/v1beta1/condition_consts.go b/api/v1beta1/condition_consts.go index 0e839b528..ac8bbcef3 100644 --- a/api/v1beta1/condition_consts.go +++ b/api/v1beta1/condition_consts.go @@ -113,6 +113,9 @@ const ( // FailedToCreatePodReason documents that managed OS version channel failed to create the synchronization pod FailedToCreatePodReason = "FailedToCreatePod" + + // ChannelDisabledReason documents that the managed OS version channel is not enabled + ChannelDisabledReason = "ChannelDisabled" ) // Managed OS Image conditions diff --git a/api/v1beta1/managedosversionchannel_types.go b/api/v1beta1/managedosversionchannel_types.go index 541ce50ca..1eade3c38 100644 --- a/api/v1beta1/managedosversionchannel_types.go +++ b/api/v1beta1/managedosversionchannel_types.go @@ -44,6 +44,9 @@ type ManagedOSVersionChannelSpec struct { // +optional // +kubebuilder:default:=false DeleteNoLongerInSyncVersions bool `json:"deleteNoLongerInSyncVersions,omitempty"` + // +optional + // +kubebuilder:default:=true + Enabled bool `json:"enabled"` // +kubebuilder:validation:Schemaless // +kubebuilder:validation:XPreserveUnknownFields // +optional diff --git a/config/crd/bases/elemental.cattle.io_managedosversionchannels.yaml b/config/crd/bases/elemental.cattle.io_managedosversionchannels.yaml index ca054b61c..0115fd559 100644 --- a/config/crd/bases/elemental.cattle.io_managedosversionchannels.yaml +++ b/config/crd/bases/elemental.cattle.io_managedosversionchannels.yaml @@ -43,6 +43,9 @@ spec: DeleteNoLongerInSyncVersions automatically deletes all no-longer-in-sync ManagedOSVersions that were created by this channel. type: boolean + enabled: + default: true + type: boolean options: x-kubernetes-preserve-unknown-fields: true syncInterval: diff --git a/controllers/managedosversionchannel_controller.go b/controllers/managedosversionchannel_controller.go index 93a56a479..f06f64aee 100644 --- a/controllers/managedosversionchannel_controller.go +++ b/controllers/managedosversionchannel_controller.go @@ -170,6 +170,29 @@ func (r *ManagedOSVersionChannelReconciler) reconcile(ctx context.Context, manag return ctrl.Result{}, nil } + if !managedOSVersionChannel.Spec.Enabled { + logger.Info("Channel is disabled. Skipping sync.") + curVersions := r.getAllOwnedManagedOSVersions(ctx, client.ObjectKey{ + Name: managedOSVersionChannel.Name, + Namespace: managedOSVersionChannel.Namespace, + }) + for _, version := range curVersions { + if err := r.deprecateVersion(ctx, *managedOSVersionChannel, version); err != nil { + return ctrl.Result{}, fmt.Errorf("Deprecating ManagedOSVersion %s: %w", version.Name, err) + } + } + if err := r.deleteSyncerPod(ctx, *managedOSVersionChannel); err != nil { + return ctrl.Result{}, fmt.Errorf("deleting syncer pod: %w", err) + } + meta.SetStatusCondition(&managedOSVersionChannel.Status.Conditions, metav1.Condition{ + Type: elementalv1.ReadyCondition, + Reason: elementalv1.ChannelDisabledReason, + Status: metav1.ConditionTrue, + Message: "Channel is disabled", + }) + return ctrl.Result{}, nil + } + reachedNextInterval := false lastSync := managedOSVersionChannel.Status.LastSyncedTime if lastSync != nil && lastSync.Add(interval).Before(time.Now()) { @@ -365,22 +388,8 @@ func (r *ManagedOSVersionChannelReconciler) createManagedOSVersions(ctx context. // 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()) - if version.ObjectMeta.Annotations == nil { - version.ObjectMeta.Annotations = map[string]string{} - } - 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) - } - if ch.Spec.DeleteNoLongerInSyncVersions { - logger.Info("Auto-deleting no longer in sync ManagedOSVersion due to channel settings", "name", version.Name) - if err := r.Delete(ctx, version); err != nil { - logger.Error(err, "Could not auto-delete no longer in sync ManagedOSVersion") - return fmt.Errorf("auto-deleting ManagedOSVersion '%s': %w", version.Name, err) - } + if err := r.deprecateVersion(ctx, *ch, version); err != nil { + return fmt.Errorf("Deprecating ManagedOSVersion %s: %w", version.Name, err) } } } @@ -388,6 +397,29 @@ func (r *ManagedOSVersionChannelReconciler) createManagedOSVersions(ctx context. return nil } +// deprecateVersion flags a ManagedOSVersion as orphan and if needed trigger its deletion. +func (r *ManagedOSVersionChannelReconciler) deprecateVersion(ctx context.Context, channel elementalv1.ManagedOSVersionChannel, version *elementalv1.ManagedOSVersion) error { + logger := ctrl.LoggerFrom(ctx).WithValues("ManagedOSVersionChannel", channel.Name).WithValues("ManagedOSVersion", version.Name) + logger.Info("ManagedOSVersion no longer synced through this channel") + patchBase := client.MergeFrom(version.DeepCopy()) + if version.ObjectMeta.Annotations == nil { + version.ObjectMeta.Annotations = map[string]string{} + } + 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") + return fmt.Errorf("deprecating ManagedOSVersion '%s': %w", version.Name, err) + } + if channel.Spec.DeleteNoLongerInSyncVersions { + logger.Info("Auto-deleting no longer in sync ManagedOSVersion due to channel settings") + if err := r.Delete(ctx, version); err != nil { + logger.Error(err, "Could not auto-delete no longer in sync ManagedOSVersion") + return fmt.Errorf("auto-deleting 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 func (r *ManagedOSVersionChannelReconciler) getAllOwnedManagedOSVersions(ctx context.Context, chKey client.ObjectKey) map[string]*elementalv1.ManagedOSVersion { logger := ctrl.LoggerFrom(ctx) @@ -485,6 +517,24 @@ func (r *ManagedOSVersionChannelReconciler) createSyncerPod(ctx context.Context, return nil } +// deleteSyncerPod deletes the syncer pod if it exists +func (r *ManagedOSVersionChannelReconciler) deleteSyncerPod(ctx context.Context, channel elementalv1.ManagedOSVersionChannel) error { + pod := &corev1.Pod{} + if err := r.Get(ctx, client.ObjectKey{ + Namespace: channel.Namespace, + Name: channel.Name, + }, pod); apierrors.IsNotFound(err) { + // Pod does not exist. Nothing to do. + return nil + } else if err != nil { + return fmt.Errorf("getting pod: %w", err) + } + if err := r.Delete(ctx, pod); err != nil { + return fmt.Errorf("deleting pod: %w", err) + } + return nil +} + // filterChannelEvents is a method that filters reconcile requests events for the channels reconciler. // ManagedOSVersionChannelReconciler watches channels and owned pods. This filter ignores pod // create/delete/generic events and only reacts on pod phase updates. Channel update events are diff --git a/controllers/managedosversionchannel_controller_test.go b/controllers/managedosversionchannel_controller_test.go index 5b305b5de..c3ee52936 100644 --- a/controllers/managedosversionchannel_controller_test.go +++ b/controllers/managedosversionchannel_controller_test.go @@ -130,6 +130,9 @@ var _ = Describe("reconcile managed os version channel", func() { Name: "test-name", Namespace: "default", }, + Spec: elementalv1.ManagedOSVersionChannelSpec{ + Enabled: true, + }, } pod = &corev1.Pod{ @@ -446,6 +449,98 @@ var _ = Describe("reconcile managed os version channel", func() { 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() { @@ -482,6 +577,9 @@ var _ = Describe("managed os version channel controller integration tests", func Name: "test-name", Namespace: "default", }, + Spec: elementalv1.ManagedOSVersionChannelSpec{ + Enabled: true, + }, } pod = &corev1.Pod{ diff --git a/tests/catalog/managedosversionchannel.go b/tests/catalog/managedosversionchannel.go index 64937d480..56c7649d5 100644 --- a/tests/catalog/managedosversionchannel.go +++ b/tests/catalog/managedosversionchannel.go @@ -34,6 +34,7 @@ func NewManagedOSVersionChannel(namespace, name, sType, interval string, options Namespace: namespace, }, Spec: elementalv1.ManagedOSVersionChannelSpec{ + Enabled: true, Type: sType, SyncInterval: interval, Options: options,