Skip to content

Commit

Permalink
Implement channel Enabled flag
Browse files Browse the repository at this point in the history
Signed-off-by: Andrea Mazzotti <[email protected]>
  • Loading branch information
anmazzotti committed Jul 25, 2024
1 parent b866419 commit 5b8c9f3
Show file tree
Hide file tree
Showing 10 changed files with 224 additions and 29 deletions.
3 changes: 3 additions & 0 deletions .obs/chartfile/crds/templates/crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
30 changes: 27 additions & 3 deletions .obs/chartfile/operator/questions.yaml
Original file line number Diff line number Diff line change
@@ -1,24 +1,48 @@
questions:
- variable: channel.defaultChannel
default: "true"
default: "false"
description: "Provide an Elemental OS Channel container image"
label: Elemental OS Channel
type: boolean
show_subquestion_if: true
group: "Elemental 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)"
type: string
label: Elemental OS Channel Image
group: "Elemental OS Channel"
- variable: channel.tag
default: "%VERSION%"
description: "Specify Elemental OS channel image tag"
type: string
label: "Elemental OS Channel Tag"
group: "Elemental OS Channel"
- variable: defaultChannels.included
default: true
description: "Provide default Elemental OS Channels. Note for air-gapped scenarios you need to provide your own OS channel image (see https://elemental.docs.rancher.com/airgap )"
label: Elemental OS Channels
type: boolean
show_subquestion_if: true
group: "Elemental OS Channels"
subquestions:
- variable: defaultChannels.includes.sle_micro_5_5
default: true
description: "Default channel that can be used for any generic workload."
type: bool
label: SLE Micro 5.5 Channel
group: "Elemental OS Channels"
- variable: defaultChannels.includes.sle_micro_5_5_kvm
default: true
description: "Ready to be used with KVM. Contains QEMU Guest agent by default."
type: bool
label: SLE Micro 5.5 KVM Channel
group: "Elemental OS Channels"
- variable: defaultChannels.includes.sle_micro_5_5_rt
default: true
description: "Channel that can be used for any generic workload with a Real-Time kernel."
type: bool
label: SLE Micro 5.5 RT Channel
group: "Elemental OS Channels"
- variable: debug
default: "false"
description: "Enable debug logging in the Elemental operator"
Expand Down
44 changes: 40 additions & 4 deletions .obs/chartfile/operator/templates/channels.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,45 @@ spec:
image: {{ .Values.channel.image }}:{{ .Values.channel.tag }}
type: custom
{{ end }}

---
{{ if and .Values.defaultChannels.included .Values.defaultChannels.includes.sle_micro_5_5 (not (lookup "elemental.cattle.io/v1beta1" "ManagedOSVersionChannel" "fleet-default" "sle-micro-5.5")) }}
apiVersion: elemental.cattle.io/v1beta1
kind: ManagedOSVersionChannel
metadata:
name: sle-micro-5.5
namespace: fleet-default
spec:
enabled: false
options:
image: registry.opensuse.org/isv/rancher/elemental/dev/containers/rancher/elemental-channel/sle-micro:5.5
type: custom
{{ end }}
---
{{ if and .Values.defaultChannels.included .Values.defaultChannels.includes.sle_micro_5_5_kvm (not (lookup "elemental.cattle.io/v1beta1" "ManagedOSVersionChannel" "fleet-default" "sle-micro-5.5-kvm")) }}
apiVersion: elemental.cattle.io/v1beta1
kind: ManagedOSVersionChannel
metadata:
name: sle-micro-5.5-kvm
namespace: fleet-default
spec:
enabled: false
options:
image: registry.opensuse.org/isv/rancher/elemental/dev/containers/rancher/elemental-channel/sle-micro:5.5-kvm
type: custom
{{ end }}
---
{{ if and .Values.defaultChannels.included .Values.defaultChannels.includes.sle_micro_5_5_rt (not (lookup "elemental.cattle.io/v1beta1" "ManagedOSVersionChannel" "fleet-default" "sle-micro-5.5-rt")) }}
apiVersion: elemental.cattle.io/v1beta1
kind: ManagedOSVersionChannel
metadata:
name: sle-micro-5.5-rt
namespace: fleet-default
spec:
enabled: false
options:
image: registry.opensuse.org/isv/rancher/elemental/dev/containers/rancher/elemental-channel/sle-micro:5.5-rt
type: custom
{{ 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 }}
Expand All @@ -23,8 +61,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 }}
13 changes: 9 additions & 4 deletions .obs/chartfile/operator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@ seedImage:
tag: "%VERSION%"
imagePullPolicy: IfNotPresent

channel:
name: "sle-micro-5.5"
image: "%%IMG_REPO%%/rancher/elemental-channel/sle-micro"
tag: "5.5"
# default Elemental channels
defaultChannels:
# default channels are included in the chart
included: true
# default channels to include
includes:
sle_micro_5_5: true
sle_micro_5_5_kvm: true
sle_micro_5_5_rt: true

# number of operator replicas to deploy
replicas: 1
Expand Down
5 changes: 3 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions api/v1beta1/condition_consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions api/v1beta1/managedosversionchannel_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
82 changes: 66 additions & 16 deletions controllers/managedosversionchannel_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down Expand Up @@ -365,29 +388,38 @@ 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)
}
}
}

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)
Expand Down Expand Up @@ -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
Expand Down
67 changes: 67 additions & 0 deletions controllers/managedosversionchannel_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -446,6 +449,67 @@ 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")
})
})

var _ = Describe("managed os version channel controller integration tests", func() {
Expand Down Expand Up @@ -482,6 +546,9 @@ var _ = Describe("managed os version channel controller integration tests", func
Name: "test-name",
Namespace: "default",
},
Spec: elementalv1.ManagedOSVersionChannelSpec{
Enabled: true,
},
}

pod = &corev1.Pod{
Expand Down

0 comments on commit 5b8c9f3

Please sign in to comment.