elemental-operator/controllers/machineinventory_controller...

796 lines
27 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"
"encoding/json"
"errors"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
elementalv1 "github.com/rancher/elemental-operator/api/v1beta1"
systemagent "github.com/rancher/elemental-operator/internal/system-agent"
"github.com/rancher/elemental-operator/pkg/network"
"github.com/rancher/elemental-operator/pkg/test"
apierrors "k8s.io/apimachinery/pkg/api/errors"
)
var _ = Describe("reconcile machine inventory", func() {
var r *MachineInventoryReconciler
var mInventory *elementalv1.MachineInventory
var planSecret *corev1.Secret
BeforeEach(func() {
r = &MachineInventoryReconciler{
Client: cl,
}
mInventory = &elementalv1.MachineInventory{
ObjectMeta: metav1.ObjectMeta{
Finalizers: []string{elementalv1.MachineInventoryFinalizer},
Name: "machine-inventory-suite",
Namespace: "default",
},
}
planSecret = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: mInventory.Namespace,
Name: mInventory.Name,
},
Data: map[string][]byte{
"applied-checksum": []byte("appliedchecksum"),
},
}
Expect(cl.Create(ctx, mInventory)).To(Succeed())
})
AfterEach(func() {
Expect(test.CleanupAndWait(ctx, cl, mInventory, planSecret)).To(Succeed())
})
It("should reconcile machine inventory object when plan secret doesn't exist", func() {
_, err := r.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: mInventory.Namespace,
Name: mInventory.Name,
},
})
Expect(err).ToNot(HaveOccurred())
Expect(cl.Get(ctx, client.ObjectKey{
Name: mInventory.Name,
Namespace: mInventory.Namespace,
}, mInventory)).To(Succeed())
cond := meta.FindStatusCondition(mInventory.Status.Conditions, elementalv1.ReadyCondition)
Expect(cond).NotTo(BeNil())
Expect(cond.Reason).To(Equal(elementalv1.WaitingForPlanReason))
Expect(cond.Status).To(Equal(metav1.ConditionFalse))
Expect(cond.Message).To(Equal("waiting for plan to be applied"))
})
It("should reconcile machine inventory object when plan secret already exists", func() {
Expect(cl.Create(ctx, planSecret)).To(Succeed())
_, err := r.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: mInventory.Namespace,
Name: mInventory.Name,
},
})
Expect(err).ToNot(HaveOccurred())
Expect(cl.Get(ctx, client.ObjectKey{
Name: mInventory.Name,
Namespace: mInventory.Namespace,
}, mInventory)).To(Succeed())
Expect(mInventory.Status.Plan.Checksum).To(Equal(string(planSecret.Data["applied-checksum"])))
Expect(mInventory.Status.Plan.State).To(Equal(elementalv1.PlanApplied))
cond := meta.FindStatusCondition(mInventory.Status.Conditions, elementalv1.ReadyCondition)
Expect(cond).NotTo(BeNil())
Expect(cond.Reason).To(Equal(elementalv1.PlanSuccessfullyAppliedReason))
Expect(cond.Status).To(Equal(metav1.ConditionTrue))
Expect(cond.Message).To(Equal("plan successfully applied"))
})
It("should add finalizer if not exist", func() {
noFinalizerMI := &elementalv1.MachineInventory{
ObjectMeta: metav1.ObjectMeta{
Name: "machine-inventory-no-finalizer",
Namespace: "default",
},
}
Expect(cl.Create(ctx, noFinalizerMI)).To(Succeed())
_, err := r.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: mInventory.Namespace,
Name: mInventory.Name,
},
})
Expect(err).ToNot(HaveOccurred())
Expect(cl.Get(ctx, client.ObjectKey{
Name: mInventory.Name,
Namespace: mInventory.Namespace,
}, noFinalizerMI)).To(Succeed())
Expect(controllerutil.ContainsFinalizer(noFinalizerMI, elementalv1.MachineInventoryFinalizer)).To(BeTrue())
Expect(test.CleanupAndWait(ctx, cl, noFinalizerMI)).To(Succeed())
})
})
var _ = Describe("createPlanSecret", func() {
var r *MachineInventoryReconciler
var mInventory *elementalv1.MachineInventory
var planSecret *corev1.Secret
BeforeEach(func() {
r = &MachineInventoryReconciler{
Client: cl,
}
mInventory = &elementalv1.MachineInventory{
ObjectMeta: metav1.ObjectMeta{
Name: "machine-inventory-suite",
Namespace: "default",
},
}
planSecret = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: mInventory.Namespace,
Name: mInventory.Name,
},
}
Expect(cl.Create(ctx, mInventory)).To(Succeed())
})
AfterEach(func() {
Expect(test.CleanupAndWait(ctx, cl, mInventory, planSecret)).To(Succeed())
})
It("should succesfully create plan secret", func() {
Expect(r.createPlanSecret(ctx, mInventory)).To(Succeed())
Expect(r.Get(ctx, types.NamespacedName{Namespace: mInventory.Namespace, Name: mInventory.Name}, planSecret)).To(Succeed())
Expect(planSecret.OwnerReferences).To(HaveLen(1))
Expect(planSecret.OwnerReferences[0].APIVersion).To(Equal(elementalv1.GroupVersion.String()))
Expect(planSecret.OwnerReferences[0].Kind).To(Equal("MachineInventory"))
Expect(planSecret.OwnerReferences[0].Name).To(Equal(mInventory.Name))
Expect(planSecret.OwnerReferences[0].UID).To(Equal(mInventory.UID))
Expect(planSecret.OwnerReferences[0].Controller).To(Equal(ptr.To(true)))
Expect(planSecret.Labels).To(HaveKey(elementalv1.ElementalManagedLabel))
Expect(planSecret.Type).To(Equal(elementalv1.PlanSecretType))
Expect(planSecret.Data).To(HaveKey("plan"))
Expect(mInventory.Status.Plan.PlanSecretRef.Name).To(Equal(mInventory.Name))
Expect(mInventory.Status.Plan.PlanSecretRef.Namespace).To(Equal(mInventory.Namespace))
cond := meta.FindStatusCondition(mInventory.Status.Conditions, elementalv1.ReadyCondition)
Expect(cond).NotTo(BeNil())
Expect(cond.Reason).To(Equal(elementalv1.WaitingForPlanReason))
Expect(cond.Status).To(Equal(metav1.ConditionFalse))
Expect(cond.Message).To(Equal("waiting for plan to be applied"))
})
It("shouldn't return error is secret already exists", func() {
Expect(r.createPlanSecret(ctx, mInventory)).To(Succeed())
Expect(r.createPlanSecret(ctx, mInventory)).To(Succeed())
})
It("should return error is secret fails to be created", func() {
r.Client = machineInventoryFailingClient{}
err := r.createPlanSecret(ctx, mInventory)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("failed to create"))
})
It("should do nothing if ready condition is present", func() {
meta.SetStatusCondition(&mInventory.Status.Conditions, metav1.Condition{
Type: elementalv1.ReadyCondition,
Reason: elementalv1.PlanSuccessfullyAppliedReason,
Status: metav1.ConditionTrue,
Message: "plan successfully applied",
})
Expect(r.createPlanSecret(ctx, mInventory)).To(Succeed())
})
It("should not reset network if configurator none", func() {
inventoryWithNoneNetwork := &elementalv1.MachineInventory{
ObjectMeta: metav1.ObjectMeta{
Name: "machine-inventory-network-none",
Namespace: "default",
},
Spec: elementalv1.MachineInventorySpec{
Network: elementalv1.NetworkConfig{
Configurator: network.ConfiguratorNone,
},
},
}
Expect(r.Create(ctx, inventoryWithNoneNetwork)).Should(Succeed())
Expect(r.createPlanSecret(ctx, inventoryWithNoneNetwork)).To(Succeed())
Expect(r.Get(ctx, types.NamespacedName{Namespace: inventoryWithNoneNetwork.Namespace, Name: inventoryWithNoneNetwork.Name}, planSecret)).To(Succeed())
Expect(r.updatePlanSecretWithReset(ctx, inventoryWithNoneNetwork, planSecret)).Should(Succeed())
Expect(r.Get(ctx, types.NamespacedName{Namespace: inventoryWithNoneNetwork.Namespace, Name: inventoryWithNoneNetwork.Name}, planSecret)).To(Succeed())
planData, found := planSecret.Data["plan"]
Expect(found).To(BeTrue(), "Secret should contain reset plan data")
plan := &systemagent.Plan{}
Expect(json.Unmarshal(planData, plan)).Should(Succeed())
Expect(len(plan.OneTimeInstructions)).Should(Equal(2), "Should contain 2 reset instructions")
Expect(plan.OneTimeInstructions).ShouldNot(ContainElement(systemagent.OneTimeInstruction{
CommonInstruction: systemagent.CommonInstruction{
Name: "restore first boot config",
Command: "elemental-register",
Args: []string{"--debug", "--reset-network"},
}}))
Expect(test.CleanupAndWait(ctx, cl, inventoryWithNoneNetwork)).To(Succeed())
})
It("should reset network if configurator not none", func() {
inventoryWithNetwork := &elementalv1.MachineInventory{
ObjectMeta: metav1.ObjectMeta{
Name: "machine-inventory-network-nmc",
Namespace: "default",
},
Spec: elementalv1.MachineInventorySpec{
Network: elementalv1.NetworkConfig{
Configurator: network.ConfiguratorNmc,
},
},
}
Expect(r.Create(ctx, inventoryWithNetwork)).Should(Succeed())
Expect(r.createPlanSecret(ctx, inventoryWithNetwork)).To(Succeed())
Expect(r.Get(ctx, types.NamespacedName{Namespace: inventoryWithNetwork.Namespace, Name: inventoryWithNetwork.Name}, planSecret)).To(Succeed())
Expect(r.updatePlanSecretWithReset(ctx, inventoryWithNetwork, planSecret)).Should(Succeed())
Expect(r.Get(ctx, types.NamespacedName{Namespace: inventoryWithNetwork.Namespace, Name: inventoryWithNetwork.Name}, planSecret)).To(Succeed())
planData, found := planSecret.Data["plan"]
Expect(found).To(BeTrue(), "Secret should contain reset plan data")
plan := &systemagent.Plan{}
Expect(json.Unmarshal(planData, plan)).Should(Succeed())
Expect(plan.OneTimeInstructions).Should(ContainElement(systemagent.OneTimeInstruction{
CommonInstruction: systemagent.CommonInstruction{
Name: "restore first boot config",
Command: "elemental-register",
Args: []string{"--debug", "--reset-network"},
}}))
Expect(test.CleanupAndWait(ctx, cl, inventoryWithNetwork)).To(Succeed())
})
})
var _ = Describe("updateInventoryWithAdoptionStatus", func() {
var r *MachineInventoryReconciler
var mInventory *elementalv1.MachineInventory
var miSelector *elementalv1.MachineInventorySelector
BeforeEach(func() {
r = &MachineInventoryReconciler{
Client: cl,
}
miSelector = &elementalv1.MachineInventorySelector{
ObjectMeta: metav1.ObjectMeta{
Name: "machine-selector",
Namespace: "default",
UID: "test",
},
}
mInventory = &elementalv1.MachineInventory{
ObjectMeta: metav1.ObjectMeta{
Name: "machine-inventory-suite",
Namespace: "default",
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: elementalv1.GroupVersion.String(),
Kind: "MachineInventorySelector",
Name: miSelector.ObjectMeta.Name,
UID: miSelector.ObjectMeta.UID,
},
},
},
}
Expect(cl.Create(ctx, mInventory)).To(Succeed())
Expect(cl.Create(ctx, miSelector)).To(Succeed())
})
AfterEach(func() {
Expect(test.CleanupAndWait(ctx, cl, mInventory, miSelector)).To(Succeed())
})
It("successfully verifies owner reference in status", func() {
// Set selector reference
Expect(cl.Get(ctx, types.NamespacedName{
Namespace: miSelector.Namespace,
Name: miSelector.Name,
}, miSelector)).To(Succeed())
miSelector.Status.MachineInventoryRef = &corev1.LocalObjectReference{
Name: mInventory.Name,
}
Expect(cl.Status().Update(ctx, miSelector)).To(Succeed())
requeue, err := r.updateInventoryWithAdoptionStatus(ctx, mInventory)
Expect(err).To(Succeed())
Expect(requeue).To(BeFalse())
cond := meta.FindStatusCondition(mInventory.Status.Conditions, elementalv1.AdoptionReadyCondition)
Expect(cond).NotTo(BeNil())
Expect(cond.Status).To(Equal(metav1.ConditionTrue))
Expect(cond.Reason).To(Equal(elementalv1.SuccessfullyAdoptedReason))
})
It("reaches adoption validation timeout", func() {
testTimeout := time.Now().Add(adoptionTimeout * 2 * time.Second)
requeue, err := r.updateInventoryWithAdoptionStatus(ctx, mInventory)
for err == nil {
Expect(err).To(Succeed())
Expect(requeue).To(BeTrue())
Expect(time.Now().Before(testTimeout)).To(BeTrue())
time.Sleep(time.Second)
requeue, err = r.updateInventoryWithAdoptionStatus(ctx, mInventory)
}
Expect(err.Error()).To(ContainSubstring("Adoption timeout"))
cond := meta.FindStatusCondition(mInventory.Status.Conditions, elementalv1.AdoptionReadyCondition)
Expect(cond).NotTo(BeNil())
Expect(cond.Status).To(Equal(metav1.ConditionUnknown))
Expect(cond.Reason).To(Equal(elementalv1.ValidatingAdoptionReason))
})
It("detects an adoption mismatch", func() {
// Set selector reference
Expect(cl.Get(ctx, types.NamespacedName{
Namespace: miSelector.Namespace,
Name: miSelector.Name,
}, miSelector)).To(Succeed())
miSelector.Status.MachineInventoryRef = &corev1.LocalObjectReference{
Name: "unexpectedName",
}
Expect(cl.Status().Update(ctx, miSelector)).To(Succeed())
requeue, err := r.updateInventoryWithAdoptionStatus(ctx, mInventory)
Expect(err).NotTo(Succeed())
Expect(err.Error()).To(ContainSubstring("Ownership mismatch"))
Expect(requeue).To(BeFalse())
})
It("has nothing if no owner is set", func() {
mInventory.ObjectMeta.OwnerReferences = []metav1.OwnerReference{}
requeue, err := r.updateInventoryWithAdoptionStatus(ctx, mInventory)
Expect(err).To(Succeed())
Expect(requeue).To(BeFalse())
cond := meta.FindStatusCondition(mInventory.Status.Conditions, elementalv1.AdoptionReadyCondition)
Expect(cond).NotTo(BeNil())
Expect(cond.Status).To(Equal(metav1.ConditionFalse))
Expect(cond.Reason).To(Equal(elementalv1.WaitingToBeAdoptedReason))
})
})
var _ = Describe("updateInventoryWithPlanStatus", func() {
var r *MachineInventoryReconciler
var mInventory *elementalv1.MachineInventory
var planSecret *corev1.Secret
BeforeEach(func() {
r = &MachineInventoryReconciler{
Client: cl,
}
mInventory = &elementalv1.MachineInventory{
ObjectMeta: metav1.ObjectMeta{
Name: "machine-inventory-suite",
Namespace: "default",
},
}
planSecret = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: mInventory.Namespace,
Name: mInventory.Name,
},
}
Expect(cl.Create(ctx, mInventory)).To(Succeed())
})
AfterEach(func() {
Expect(test.CleanupAndWait(ctx, cl, mInventory, planSecret)).To(Succeed())
})
It("should succesfully update when plan is applied", func() {
planSecret.Data = map[string][]byte{
"applied-checksum": []byte("applied-checksum"),
}
Expect(cl.Create(ctx, planSecret)).To(Succeed())
mInventory.Status.Plan = &elementalv1.PlanStatus{}
Expect(r.updateInventoryWithPlanStatus(ctx, mInventory)).To(Succeed())
Expect(mInventory.Status.Plan.State).To(Equal(elementalv1.PlanApplied))
Expect(mInventory.Status.Conditions).To(HaveLen(1))
Expect(mInventory.Status.Conditions[0].Type).To(Equal(elementalv1.ReadyCondition))
Expect(mInventory.Status.Conditions[0].Reason).To(Equal(elementalv1.PlanSuccessfullyAppliedReason))
Expect(mInventory.Status.Conditions[0].Status).To(Equal(metav1.ConditionTrue))
Expect(mInventory.Status.Conditions[0].Message).To(Equal("plan successfully applied"))
})
It("should return error when plan failed to be applied", func() {
planSecret.Data = map[string][]byte{
"failed-checksum": []byte("failed-checksum"),
}
Expect(cl.Create(ctx, planSecret)).To(Succeed())
mInventory.Status.Plan = &elementalv1.PlanStatus{}
err := r.updateInventoryWithPlanStatus(ctx, mInventory)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("failed to apply plan"))
})
AfterEach(func() {
Expect(test.CleanupAndWait(ctx, cl, mInventory, planSecret)).To(Succeed())
})
})
var _ = Describe("handle finalizer", func() {
var r *MachineInventoryReconciler
var mInventory *elementalv1.MachineInventory
var planSecret *corev1.Secret
BeforeEach(func() {
r = &MachineInventoryReconciler{
Client: cl,
}
mInventory = &elementalv1.MachineInventory{
ObjectMeta: metav1.ObjectMeta{
DeletionTimestamp: &metav1.Time{Time: time.Now()},
Annotations: map[string]string{elementalv1.MachineInventoryResettableAnnotation: "true"},
Finalizers: []string{elementalv1.MachineInventoryFinalizer},
Name: "machine-inventory-suite-finalizer",
Namespace: "default",
},
}
planSecret = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: mInventory.Name,
Namespace: mInventory.Namespace,
},
}
// 1. Create initial MachineInventory
Expect(cl.Create(ctx, mInventory)).To(Succeed())
// 2. Create initial plan Secret
_, err := r.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: mInventory.Namespace,
Name: mInventory.Name,
},
})
Expect(err).ToNot(HaveOccurred())
// 3. Update meta.DeletionTimestamp
Expect(cl.Delete(ctx, mInventory)).To(Succeed())
// 4. Update secret with reset plan
_, err = r.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: mInventory.Namespace,
Name: mInventory.Name,
},
})
Expect(err).ToNot(HaveOccurred())
// 5. Update MachineInventory plan status
_, err = r.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: mInventory.Namespace,
Name: mInventory.Name,
},
})
Expect(err).ToNot(HaveOccurred())
})
It("should update secret with reset plan", func() {
Expect(cl.Get(ctx, client.ObjectKey{
Name: planSecret.Name,
Namespace: planSecret.Namespace,
}, planSecret)).To(Succeed())
Expect(cl.Get(ctx, client.ObjectKey{
Name: mInventory.Name,
Namespace: mInventory.Namespace,
}, mInventory)).To(Succeed())
wantChecksum, wantPlan, err := r.newResetPlan(ctx, false)
Expect(err).ToNot(HaveOccurred())
// Check Plan status
Expect(mInventory.Status.Plan.Checksum).To(Equal(wantChecksum))
Expect(mInventory.Status.Plan.PlanSecretRef.Name).To(Equal(planSecret.Name))
Expect(mInventory.Status.Plan.PlanSecretRef.Namespace).To(Equal(planSecret.Namespace))
Expect(mInventory.Status.Plan.State).To(Equal(elementalv1.PlanState("")))
// Check MachineInventory status
Expect(mInventory.Status.Conditions[0].Type).To(Equal(elementalv1.ReadyCondition))
Expect(mInventory.Status.Conditions[0].Reason).To(Equal(elementalv1.WaitingForPlanReason))
Expect(mInventory.Status.Conditions[0].Status).To(Equal(metav1.ConditionFalse))
Expect(mInventory.Status.Conditions[0].Message).To(Equal("waiting for plan to be applied"))
// Check plan secret was updated
Expect(planSecret.Annotations[elementalv1.PlanTypeAnnotation]).To(Equal(elementalv1.PlanTypeReset))
Expect(planSecret.Data["plan"]).To(Equal(wantPlan))
Expect(planSecret.Data["applied-checksum"]).To(Equal([]byte("")))
Expect(planSecret.Data["failed-checksum"]).To(Equal([]byte("")))
// Check we are holding on the MachineInventory (preventing actual deletion)
_, err = r.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: mInventory.Namespace,
Name: mInventory.Name,
},
})
Expect(err).ToNot(HaveOccurred())
Expect(cl.Get(ctx, client.ObjectKey{
Name: mInventory.Name,
Namespace: mInventory.Namespace,
}, mInventory)).To(Succeed())
})
It("should remove finalizer on reset plan applied", func() {
// 6. Mark the reset plan as applied
Expect(cl.Get(ctx, client.ObjectKey{
Name: mInventory.Name,
Namespace: mInventory.Namespace,
}, planSecret)).To(Succeed())
planSecret.Data["applied-checksum"] = []byte("applied")
Expect(cl.Update(ctx, planSecret)).To(Succeed())
// 7. Trigger finalizer removal
_, err := r.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: mInventory.Namespace,
Name: mInventory.Name,
},
})
Expect(err).ToNot(HaveOccurred())
// Check MachineInventory was actually deleted
err = cl.Get(ctx, client.ObjectKey{
Name: mInventory.Name,
Namespace: mInventory.Namespace,
}, mInventory)
Expect(apierrors.IsNotFound(err)).To(BeTrue())
})
It("should delete by removing finalizer when resettable annotation is false", func() {
// 6. Manually disable resettable annotation
Expect(cl.Get(ctx, client.ObjectKey{
Name: mInventory.Name,
Namespace: mInventory.Namespace,
}, mInventory)).To(Succeed())
mInventory.Annotations[elementalv1.MachineInventoryResettableAnnotation] = "false"
Expect(cl.Update(ctx, mInventory)).To(Succeed())
// 7. Trigger finalizer removal
_, err := r.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: mInventory.Namespace,
Name: mInventory.Name,
},
})
Expect(err).ToNot(HaveOccurred())
// Check MachineInventory was actually deleted
err = cl.Get(ctx, client.ObjectKey{
Name: mInventory.Name,
Namespace: mInventory.Namespace,
}, mInventory)
Expect(apierrors.IsNotFound(err)).To(BeTrue())
})
It("should delete by removing finalizer when up for deletion", func() {
// 6. Manually remove finalizer
Expect(cl.Get(ctx, client.ObjectKey{
Name: mInventory.Name,
Namespace: mInventory.Namespace,
}, mInventory)).To(Succeed())
controllerutil.RemoveFinalizer(mInventory, elementalv1.MachineInventoryFinalizer)
Expect(cl.Update(ctx, mInventory)).To(Succeed())
// Check MachineInventory was actually deleted
err := cl.Get(ctx, client.ObjectKey{
Name: mInventory.Name,
Namespace: mInventory.Namespace,
}, mInventory)
Expect(apierrors.IsNotFound(err)).To(BeTrue())
})
AfterEach(func() {
Expect(test.CleanupAndWait(ctx, cl, mInventory, planSecret)).To(Succeed())
})
})
var _ = Describe("handle unmanaged finalizer", func() {
var r *MachineInventoryReconciler
var mInventory *elementalv1.MachineInventory
var planSecret *corev1.Secret
BeforeEach(func() {
r = &MachineInventoryReconciler{
Client: cl,
}
mInventory = &elementalv1.MachineInventory{
ObjectMeta: metav1.ObjectMeta{
DeletionTimestamp: &metav1.Time{Time: time.Now()},
Annotations: map[string]string{elementalv1.MachineInventoryResettableAnnotation: "true", elementalv1.MachineInventoryOSUnmanagedAnnotation: "true"},
Finalizers: []string{elementalv1.MachineInventoryFinalizer},
Name: "machine-inventory-suite-finalizer",
Namespace: "default",
},
}
planSecret = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: mInventory.Name,
Namespace: mInventory.Namespace,
},
}
// 1. Create initial MachineInventory
Expect(cl.Create(ctx, mInventory)).To(Succeed())
// 2. Create initial plan Secret
_, err := r.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: mInventory.Namespace,
Name: mInventory.Name,
},
})
Expect(err).ToNot(HaveOccurred())
// 3. Update meta.DeletionTimestamp
Expect(cl.Delete(ctx, mInventory)).To(Succeed())
// 4. Update secret with reset plan
_, err = r.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: mInventory.Namespace,
Name: mInventory.Name,
},
})
Expect(err).ToNot(HaveOccurred())
// 5. Update MachineInventory plan status
_, err = r.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: mInventory.Namespace,
Name: mInventory.Name,
},
})
Expect(err).ToNot(HaveOccurred())
})
It("should update secret with reset plan when unmanaged annotation is true", func() {
// Manually enable os.unmanaged annotation
Expect(cl.Get(ctx, client.ObjectKey{
Name: planSecret.Name,
Namespace: planSecret.Namespace,
}, planSecret)).To(Succeed())
Expect(cl.Get(ctx, client.ObjectKey{
Name: mInventory.Name,
Namespace: mInventory.Namespace,
}, mInventory)).To(Succeed())
mInventory.Annotations[elementalv1.MachineInventoryOSUnmanagedAnnotation] = "true"
Expect(cl.Update(ctx, mInventory)).To(Succeed())
_, wantPlan, err := r.newUnmanagedResetPlan(ctx)
Expect(err).ToNot(HaveOccurred())
// Check Plan status
Expect(mInventory.Status.Plan.PlanSecretRef.Name).To(Equal(planSecret.Name))
Expect(mInventory.Status.Plan.PlanSecretRef.Namespace).To(Equal(planSecret.Namespace))
Expect(mInventory.Status.Plan.State).To(Equal(elementalv1.PlanState("")))
// Check MachineInventory status
Expect(mInventory.Status.Conditions[0].Type).To(Equal(elementalv1.ReadyCondition))
Expect(mInventory.Status.Conditions[0].Reason).To(Equal(elementalv1.WaitingForPlanReason))
Expect(mInventory.Status.Conditions[0].Status).To(Equal(metav1.ConditionFalse))
Expect(mInventory.Status.Conditions[0].Message).To(Equal("waiting for plan to be applied"))
// Check plan secret was updated
Expect(planSecret.Annotations[elementalv1.PlanTypeAnnotation]).To(Equal(elementalv1.PlanTypeReset))
Expect(string(planSecret.Data["plan"])).To(Equal(string(wantPlan)))
Expect(string(planSecret.Data["applied-checksum"])).To(Equal(""))
Expect(string(planSecret.Data["failed-checksum"])).To(Equal(""))
// Check we are holding on the MachineInventory (preventing actual deletion)
_, err = r.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: mInventory.Namespace,
Name: mInventory.Name,
},
})
Expect(err).ToNot(HaveOccurred())
Expect(cl.Get(ctx, client.ObjectKey{
Name: mInventory.Name,
Namespace: mInventory.Namespace,
}, mInventory)).To(Succeed())
})
It("should remove finalizer on unmanaged reset plan applied", func() {
// 6. Mark the reset plan as applied
Expect(cl.Get(ctx, client.ObjectKey{
Name: mInventory.Name,
Namespace: mInventory.Namespace,
}, planSecret)).To(Succeed())
mInventory.Annotations[elementalv1.MachineInventoryOSUnmanagedAnnotation] = "true"
planSecret.Data["applied-checksum"] = []byte("applied")
Expect(cl.Update(ctx, planSecret)).To(Succeed())
// 7. Trigger finalizer removal
_, err := r.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: mInventory.Namespace,
Name: mInventory.Name,
},
})
Expect(err).ToNot(HaveOccurred())
// Check MachineInventory was actually deleted
err = cl.Get(ctx, client.ObjectKey{
Name: mInventory.Name,
Namespace: mInventory.Namespace,
}, mInventory)
Expect(apierrors.IsNotFound(err)).To(BeTrue())
})
AfterEach(func() {
Expect(test.CleanupAndWait(ctx, cl, mInventory, planSecret)).To(Succeed())
})
})
type machineInventoryFailingClient struct {
client.Client
}
func (cl machineInventoryFailingClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
return errors.New("failed to create")
}