Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions pkg/providers/vsphere/vmlifecycle/update_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ func ReconcileStatus(

var errs []error

errs = append(errs, reconcileStatusAnno2Conditions(vmCtx, k8sClient, vcVM, data)...)
errs = append(errs, reconcileStatusClass(vmCtx, k8sClient, vcVM, data)...)
errs = append(errs, reconcileStatusPowerState(vmCtx, k8sClient, vcVM, data)...)
errs = append(errs, reconcileStatusIdentifiers(vmCtx, k8sClient, vcVM, data)...)
Expand Down Expand Up @@ -91,6 +92,53 @@ func ReconcileStatus(
return apierrorsutil.NewAggregate(errs)
}

var anno2ConditionRegex = regexp.MustCompile(`^condition.vmoperator.vmware.com.protected/(.+)?$`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more of a nit: but do we not have a standard about the ordering of items in a file: consts, vars, types, funcs?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it also be in the constant file with the other annotations?


// reconcileStatusAnno2Conditions sets conditions on the VM based on
// annotation values.
func reconcileStatusAnno2Conditions(
vmCtx pkgctx.VirtualMachineContext,
_ ctrlclient.Client,
_ *object.VirtualMachine,
_ ReconcileStatusData) []error { //nolint:unparam

for k, v := range vmCtx.VM.Annotations {
if anno2ConditionRegex.MatchString(k) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the regex with the capture group when not using that, vs strings.HasPrefix()?

var (
t string
s metav1.ConditionStatus
r string
m string
)
p := strings.Split(v, ";")
if len(p) > 0 {
t = p[0]
}
if len(p) > 1 {
s = metav1.ConditionStatus(p[1])
}
if len(p) > 2 {
r = p[2]
}
if len(p) > 3 {
m = p[3]
}
if t != "" {
switch s {
case metav1.ConditionFalse:
conditions.MarkFalse(vmCtx.VM, t, r, m+"%s", "")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

..., "%s", m) didn't work?

case metav1.ConditionTrue:
conditions.MarkTrue(vmCtx.VM, t)
default:
conditions.MarkUnknown(vmCtx.VM, t, r, m+"%s", "")
}
}
}
}

return nil
}

func reconcileStatusClass(
vmCtx pkgctx.VirtualMachineContext,
k8sClient ctrlclient.Client,
Expand Down
176 changes: 176 additions & 0 deletions pkg/providers/vsphere/vmlifecycle/update_status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,182 @@ var _ = Describe("UpdateStatus", func() {
})
})

Context("Annotations to Conditions", func() {
const (
testAnnotationKey = "condition.vmoperator.vmware.com.protected/MyCondition"
testConditionType = "MyConditionType"
testReason = "MyReason"
testMessage = "My message"
)

Context("When annotation matches the protected condition pattern", func() {
When("annotation value has type and status=True", func() {
BeforeEach(func() {
vmCtx.VM.Annotations = map[string]string{
testAnnotationKey: testConditionType + ";True",
}
})
It("should set condition to True", func() {
cond := conditions.Get(vmCtx.VM, testConditionType)
Expect(cond).ToNot(BeNil())
Expect(cond.Status).To(Equal(metav1.ConditionTrue))
})
})

When("annotation value has type and status=False with reason and message", func() {
BeforeEach(func() {
vmCtx.VM.Annotations = map[string]string{
testAnnotationKey: testConditionType + ";False;" + testReason + ";" + testMessage,
}
})
It("should set condition to False with reason and message", func() {
cond := conditions.Get(vmCtx.VM, testConditionType)
Expect(cond).ToNot(BeNil())
Expect(cond.Status).To(Equal(metav1.ConditionFalse))
Expect(cond.Reason).To(Equal(testReason))
Expect(cond.Message).To(Equal(testMessage))
})
})

When("annotation value has type and status=Unknown with reason and message", func() {
BeforeEach(func() {
vmCtx.VM.Annotations = map[string]string{
testAnnotationKey: testConditionType + ";Unknown;UnknownReason;Unknown message",
}
})
It("should set condition to Unknown with reason and message", func() {
cond := conditions.Get(vmCtx.VM, testConditionType)
Expect(cond).ToNot(BeNil())
Expect(cond.Status).To(Equal(metav1.ConditionUnknown))
Expect(cond.Reason).To(Equal("UnknownReason"))
Expect(cond.Message).To(Equal("Unknown message"))
})
})

When("annotation value has type and unrecognized status", func() {
BeforeEach(func() {
vmCtx.VM.Annotations = map[string]string{
testAnnotationKey: testConditionType + ";InvalidStatus;" + testReason + ";" + testMessage,
}
})
It("should set condition to Unknown", func() {
cond := conditions.Get(vmCtx.VM, testConditionType)
Expect(cond).ToNot(BeNil())
Expect(cond.Status).To(Equal(metav1.ConditionUnknown))
})
})

When("annotation value has type but empty status", func() {
BeforeEach(func() {
vmCtx.VM.Annotations = map[string]string{
testAnnotationKey: testConditionType + ";;" + testReason + ";" + testMessage,
}
})
It("should set condition to Unknown", func() {
cond := conditions.Get(vmCtx.VM, testConditionType)
Expect(cond).ToNot(BeNil())
Expect(cond.Status).To(Equal(metav1.ConditionUnknown))
})
})

When("annotation value has only type", func() {
BeforeEach(func() {
vmCtx.VM.Annotations = map[string]string{
testAnnotationKey: testConditionType,
}
})
It("should set condition to Unknown", func() {
cond := conditions.Get(vmCtx.VM, testConditionType)
Expect(cond).ToNot(BeNil())
Expect(cond.Status).To(Equal(metav1.ConditionUnknown))
})
})

When("annotation value is empty", func() {
BeforeEach(func() {
vmCtx.VM.Annotations = map[string]string{
testAnnotationKey: "",
}
})
It("should not set any condition", func() {
cond := conditions.Get(vmCtx.VM, "")
Expect(cond).To(BeNil())
})
})

When("annotation value has no type (empty type)", func() {
BeforeEach(func() {
vmCtx.VM.Annotations = map[string]string{
testAnnotationKey: ";True;" + testReason + ";" + testMessage,
}
})
It("should not set a condition for empty type", func() {
// Since type is empty, no condition with empty type should be set
// The function checks if t != "" before setting conditions
cond := conditions.Get(vmCtx.VM, "")
Expect(cond).To(BeNil())
// But Created condition should still exist
cond = conditions.Get(vmCtx.VM, vmopv1.VirtualMachineConditionCreated)
Expect(cond).ToNot(BeNil())
})
})

When("multiple protected condition annotations exist", func() {
BeforeEach(func() {
vmCtx.VM.Annotations = map[string]string{
"condition.vmoperator.vmware.com.protected/Condition1": "Type1;True",
"condition.vmoperator.vmware.com.protected/Condition2": "Type2;False;Reason2;Message2",
"condition.vmoperator.vmware.com.protected/Condition3": "Type3;Unknown;Reason3;Message3",
}
})
It("should set all conditions appropriately", func() {
cond1 := conditions.Get(vmCtx.VM, "Type1")
Expect(cond1).ToNot(BeNil())
Expect(cond1.Status).To(Equal(metav1.ConditionTrue))

cond2 := conditions.Get(vmCtx.VM, "Type2")
Expect(cond2).ToNot(BeNil())
Expect(cond2.Status).To(Equal(metav1.ConditionFalse))
Expect(cond2.Reason).To(Equal("Reason2"))
Expect(cond2.Message).To(Equal("Message2"))

cond3 := conditions.Get(vmCtx.VM, "Type3")
Expect(cond3).ToNot(BeNil())
Expect(cond3.Status).To(Equal(metav1.ConditionUnknown))
Expect(cond3.Reason).To(Equal("Reason3"))
Expect(cond3.Message).To(Equal("Message3"))
})
})
})

Context("When annotation does not match the protected condition pattern", func() {
When("annotation has different prefix", func() {
BeforeEach(func() {
vmCtx.VM.Annotations = map[string]string{
"other.annotation/MyCondition": testConditionType + ";True",
}
})
It("should not set any condition from this annotation", func() {
cond := conditions.Get(vmCtx.VM, testConditionType)
Expect(cond).To(BeNil())
})
})

When("no annotations exist", func() {
BeforeEach(func() {
vmCtx.VM.Annotations = nil
})
It("should have default conditions set by ReconcileStatus", func() {
// ReconcileStatus sets multiple conditions including Created and ReconcileReady
// Just verify the Created condition exists and no annotation-based conditions
cond := conditions.Get(vmCtx.VM, vmopv1.VirtualMachineConditionCreated)
Expect(cond).ToNot(BeNil())
Expect(cond.Status).To(Equal(metav1.ConditionTrue))
})
})
})
})

Context("Network", func() {

Context("PrimaryIP", func() {
Expand Down
58 changes: 47 additions & 11 deletions webhooks/virtualmachine/validation/virtualmachine_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -1988,6 +1988,52 @@ func (v validator) validateAvailabilityZone(
return allErrs
}

// protectedAnnotationRegex matches annotations with keys matching the pattern:
// ^.+\.protected(/.+)?$
//
// Examples that match:
// - fu.bar.protected
// - hello.world.protected/sub-key
// - vmoperator.vmware.com.protected/reconcile-priority
//
// Examples that do NOT match:
// - protected.fu.bar
// - hello.world.protected.against/sub-key
var protectedAnnotationRegex = regexp.MustCompile(`^.+\.protected(/.*)?$`)

// validateProtectedAnnotations validates that annotations matching the
// protected annotation pattern can only be modified by privileged users.
func (v validator) validateProtectedAnnotations(vm, oldVM *vmopv1.VirtualMachine) field.ErrorList {
var allErrs field.ErrorList
annotationPath := field.NewPath("metadata", "annotations")

// Collect all protected annotation keys from both old and new VMs
protectedKeys := make(map[string]struct{})

for k := range vm.Annotations {
if protectedAnnotationRegex.MatchString(k) {
protectedKeys[k] = struct{}{}
}
}

for k := range oldVM.Annotations {
if protectedAnnotationRegex.MatchString(k) {
protectedKeys[k] = struct{}{}
}
}

// Check if any protected annotations have been modified
for k := range protectedKeys {
if vm.Annotations[k] != oldVM.Annotations[k] {
allErrs = append(allErrs, field.Forbidden(
annotationPath.Key(k),
modifyAnnotationNotAllowedForNonAdmin))
}
}

return allErrs
}

func (v validator) validateAnnotation(ctx *pkgctx.WebhookRequestContext, vm, oldVM *vmopv1.VirtualMachine) field.ErrorList {
var allErrs field.ErrorList

Expand All @@ -2012,9 +2058,7 @@ func (v validator) validateAnnotation(ctx *pkgctx.WebhookRequestContext, vm, old
oldVM = &vmopv1.VirtualMachine{}
}

if vm.Annotations[pkgconst.ReconcilePriorityAnnotationKey] != oldVM.Annotations[pkgconst.ReconcilePriorityAnnotationKey] {
allErrs = append(allErrs, field.Forbidden(annotationPath.Key(pkgconst.ReconcilePriorityAnnotationKey), modifyAnnotationNotAllowedForNonAdmin))
}
allErrs = append(allErrs, v.validateProtectedAnnotations(vm, oldVM)...)

if vm.Annotations[vmopv1.InstanceIDAnnotation] != oldVM.Annotations[vmopv1.InstanceIDAnnotation] {
allErrs = append(allErrs, field.Forbidden(annotationPath.Key(vmopv1.InstanceIDAnnotation), modifyAnnotationNotAllowedForNonAdmin))
Expand All @@ -2036,14 +2080,6 @@ func (v validator) validateAnnotation(ctx *pkgctx.WebhookRequestContext, vm, old
allErrs = append(allErrs, field.Forbidden(annotationPath.Key(vmopv1.ImportedVMAnnotation), modifyAnnotationNotAllowedForNonAdmin))
}

if vm.Annotations[pkgconst.SkipDeletePlatformResourceKey] != oldVM.Annotations[pkgconst.SkipDeletePlatformResourceKey] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we remove the annotations from the constant file since they are only used in the tests now?

allErrs = append(allErrs, field.Forbidden(annotationPath.Key(pkgconst.SkipDeletePlatformResourceKey), modifyAnnotationNotAllowedForNonAdmin))
}

if vm.Annotations[pkgconst.ApplyPowerStateTimeAnnotation] != oldVM.Annotations[pkgconst.ApplyPowerStateTimeAnnotation] {
allErrs = append(allErrs, field.Forbidden(annotationPath.Key(pkgconst.ApplyPowerStateTimeAnnotation), modifyAnnotationNotAllowedForNonAdmin))
}

for k := range anno2extraconfig.AnnotationsToExtraConfigKeys {
if vm.Annotations[k] != oldVM.Annotations[k] {
allErrs = append(allErrs, field.Forbidden(annotationPath.Key(k), modifyAnnotationNotAllowedForNonAdmin))
Expand Down
Loading