Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
104 commits
Select commit Hold shift + click to select a range
7c4df85
wip
loktev-d Feb 27, 2026
f5099bd
Merge remote-tracking branch 'origin/main' into feat/vm/hotplug-block…
loktev-d Feb 27, 2026
ddf6e44
wip
loktev-d Feb 27, 2026
ccbe3a4
wip
loktev-d Feb 27, 2026
b46c4ab
wip
loktev-d Mar 2, 2026
a04c63e
wip
loktev-d Mar 2, 2026
331ba6a
wip
loktev-d Mar 2, 2026
eb51b47
wip
loktev-d Mar 2, 2026
6312d9f
fix linter errors
loktev-d Mar 2, 2026
6c6d5e6
Merge branch 'main' into feat/vm/hotplug-block-devices-via-spec
loktev-d Mar 3, 2026
bebae37
move attachment service back
loktev-d Mar 3, 2026
2b40420
wip
loktev-d Mar 3, 2026
1a9ea6f
wip
loktev-d Mar 3, 2026
625783e
Merge branch 'main' into feat/vm/hotplug-block-devices-via-spec
loktev-d Mar 3, 2026
9f0b015
wip
loktev-d Mar 3, 2026
ababbb2
wip
loktev-d Mar 3, 2026
fab1cb0
add e2e
loktev-d Mar 4, 2026
3f9bbe1
add unit tests
loktev-d Mar 4, 2026
9fa1597
add doc
loktev-d Mar 4, 2026
e4d602e
wip
loktev-d Mar 4, 2026
d015889
fix vmbda error
loktev-d Mar 4, 2026
a8fc9cf
add validation for conflicts
loktev-d Mar 5, 2026
1843d0e
docs: review
prismagod Mar 10, 2026
7e25cbc
Merge remote-tracking branch 'origin/main' into feat/vm/hotplug-block…
loktev-d Mar 11, 2026
417b093
fix merge
loktev-d Mar 11, 2026
69e74ad
Merge remote-tracking branch 'origin/main' into feat/vm/hotplug-block…
loktev-d Mar 13, 2026
8e9a8a1
docs: update user guide for block device attachment methods
prismagod Mar 13, 2026
ace7f4c
docs: fix
prismagod Mar 13, 2026
a29e584
docs: add additional info from ADR
prismagod Mar 13, 2026
bfc9a1b
docs: post-review updates
prismagod Mar 13, 2026
de41a9a
fix backward compatility
loktev-d Mar 13, 2026
6ce1e96
fix comments
loktev-d Mar 13, 2026
f9ec5d8
wip
loktev-d Mar 13, 2026
3823407
fix: golangci-lint error
Mar 17, 2026
4861047
docs: update bootOrder description
prismagod Mar 18, 2026
981aa64
fix: vi on pvc is not ephemeral
Mar 19, 2026
d461779
feat: add bootOrder to vm block device refs status
Mar 20, 2026
919e4e7
chore: refactor bootOrder status
Mar 20, 2026
4d87095
chore: refactor bootOrderStatus 2
Mar 20, 2026
e4c33e7
wip
loktev-d Mar 23, 2026
f6ce136
add tests
loktev-d Mar 23, 2026
a9f6c87
wip
loktev-d Mar 23, 2026
4a79c41
wip
loktev-d Mar 23, 2026
b3603fe
Revert "wip"
loktev-d Mar 23, 2026
97c9376
wip
loktev-d Mar 23, 2026
4028c4c
wip
loktev-d Mar 23, 2026
193316a
fix linter errors
loktev-d Mar 23, 2026
325c879
wip
loktev-d Mar 23, 2026
afa7501
wip
loktev-d Mar 24, 2026
d821f2d
fix linter errors
loktev-d Mar 24, 2026
9fb758b
fix linter errors
loktev-d Mar 24, 2026
5546e58
wip
loktev-d Mar 25, 2026
39ceb62
wip
loktev-d Mar 25, 2026
494a45f
wip
loktev-d Apr 2, 2026
cf571b3
Merge branch 'main' into feat/vm/hotplug-block-devices-via-spec
loktev-d Apr 2, 2026
b317327
Merge branch 'feat/vm/hotplug-block-devices-via-spec' into fix/vm/pv-…
loktev-d Apr 2, 2026
16aad3d
Merge branch 'fix/vm/pv-node-affinity-scheduling' into feat/vm/disk-n…
loktev-d Apr 2, 2026
31cd2d7
fix logic for disk buses
loktev-d Apr 13, 2026
98362b1
Merge branch 'main' into feat/vm/hotplug-block-devices-via-spec
loktev-d Apr 13, 2026
e7d2151
wip
loktev-d Apr 13, 2026
a10a634
fix linter errors
loktev-d Apr 13, 2026
ee73fce
wip
loktev-d Apr 13, 2026
615087f
Merge branch 'main' into feat/vm/hotplug-block-devices-via-spec
loktev-d Apr 13, 2026
f809e17
wip
loktev-d Apr 13, 2026
0940704
wip
loktev-d Apr 13, 2026
151cea8
wip
loktev-d Apr 13, 2026
3426283
update docs
loktev-d Apr 13, 2026
ae66847
update docs
loktev-d Apr 13, 2026
e0c378d
update docs
loktev-d Apr 13, 2026
2efb5fd
Merge branch 'feat/vm/hotplug-block-devices-via-spec' into fix/vm/pv-…
loktev-d Apr 13, 2026
aaa226f
Merge branch 'fix/vm/pv-node-affinity-scheduling' into feat/vm/disk-n…
loktev-d Apr 13, 2026
e31e2af
docs: update user guide on disk hotplug and paravirtualization
prismagod Apr 15, 2026
ee8e0e3
docs: fix user guide
prismagod Apr 15, 2026
5e9f7b5
Merge branch 'main' into feat/vm/hotplug-block-devices-via-spec
loktev-d Apr 17, 2026
2ec90ef
Merge branch 'feat/vm/hotplug-block-devices-via-spec' into fix/vm/pv-…
loktev-d Apr 17, 2026
0089531
Merge branch 'fix/vm/pv-node-affinity-scheduling' into feat/vm/disk-n…
loktev-d Apr 17, 2026
53028f6
fix comments
loktev-d Apr 17, 2026
d636d04
Merge branch 'fix/vm/pv-node-affinity-scheduling' into feat/vm/disk-n…
loktev-d Apr 17, 2026
1536e5f
handler errs
loktev-d Apr 17, 2026
e39ad84
add unit tests
loktev-d Apr 17, 2026
22c1437
fix comments
loktev-d Apr 20, 2026
aa95915
Merge branch 'fix/vm/pv-node-affinity-scheduling' into feat/vm/disk-n…
loktev-d Apr 21, 2026
09237f4
wip
loktev-d Apr 23, 2026
e7f22ba
wip
loktev-d Apr 23, 2026
941395e
Merge branch 'main' into feat/vm/hotplug-block-devices-via-spec
loktev-d Apr 23, 2026
8317c2f
Merge branch 'feat/vm/hotplug-block-devices-via-spec' into fix/vm/pv-…
loktev-d Apr 23, 2026
4b96c81
Merge branch 'fix/vm/pv-node-affinity-scheduling' into feat/vm/disk-n…
loktev-d Apr 23, 2026
b446286
fix linter errors
loktev-d Apr 24, 2026
d2c413a
wip
loktev-d Apr 24, 2026
8c11288
Merge branch 'feat/vm/hotplug-block-devices-via-spec' into fix/vm/pv-…
loktev-d Apr 24, 2026
15a4044
Merge branch 'fix/vm/pv-node-affinity-scheduling' into feat/vm/disk-n…
loktev-d Apr 24, 2026
948a723
wip
loktev-d Apr 30, 2026
9569ab2
wip
loktev-d Apr 30, 2026
0593ac2
Merge branch 'fix/vm/pv-node-affinity-scheduling' into feat/vm/disk-n…
loktev-d Apr 30, 2026
3ec1de2
wip
loktev-d May 6, 2026
1dc565c
Merge branch 'main' into feat/vm/hotplug-block-devices-via-spec
loktev-d May 6, 2026
e628dba
Merge branch 'feat/vm/hotplug-block-devices-via-spec' into fix/vm/pv-…
loktev-d May 6, 2026
0ddb172
Merge branch 'fix/vm/pv-node-affinity-scheduling' into feat/vm/disk-n…
loktev-d May 6, 2026
c82a573
remove fromCacheVersion
loktev-d May 7, 2026
330bf14
wip
loktev-d May 7, 2026
2b637e7
add label for e2e test
loktev-d May 7, 2026
90dcffe
Merge branch 'feat/vm/hotplug-block-devices-via-spec' into fix/vm/pv-…
loktev-d May 7, 2026
b7ce95d
Merge branch 'fix/vm/pv-node-affinity-scheduling' into feat/vm/disk-n…
loktev-d May 7, 2026
543f3d1
Merge branch 'main' into feat/vm/disk-node-availability-webhook
loktev-d May 15, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,15 @@ limitations under the License.

package nodeaffinity

import corev1 "k8s.io/api/core/v1"
import (
"fmt"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"
corev1helpers "k8s.io/component-helpers/scheduling/corev1"

"github.com/deckhouse/virtualization/api/core/v1alpha2"
)

func IntersectTerms(perPVTerms [][]corev1.NodeSelectorTerm) []corev1.NodeSelectorTerm {
if len(perPVTerms) == 0 {
Expand All @@ -29,6 +37,69 @@ func IntersectTerms(perPVTerms [][]corev1.NodeSelectorTerm) []corev1.NodeSelecto
return result
}

func MatchesVMPlacement(node *corev1.Node, vm *v1alpha2.VirtualMachine, vmClass *v1alpha2.VirtualMachineClass) (bool, error) {
if !matchesNodeSelector(node, vm.Spec.NodeSelector) {
return false, nil
}
match, err := matchesVMAffinity(node, vm.Spec.Affinity)
if err != nil {
return false, fmt.Errorf("match VM affinity: %w", err)
}
if !match {
return false, nil
}
match, err = matchesVMClassNodeSelector(node, vmClass)
if err != nil {
return false, fmt.Errorf("match VM class node selector: %w", err)
}
if !match {
return false, nil
}
return toleratesNodeTaints(node, vm.Spec.Tolerations), nil
}

func matchesNodeSelector(node *corev1.Node, nodeSelector map[string]string) bool {
if len(nodeSelector) == 0 {
return true
}
return labels.SelectorFromSet(nodeSelector).Matches(labels.Set(node.Labels))
}

func matchesVMAffinity(node *corev1.Node, affinity *v1alpha2.VMAffinity) (bool, error) {
if affinity == nil || affinity.NodeAffinity == nil ||
affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution == nil {
return true, nil
}
return corev1helpers.MatchNodeSelectorTerms(node, affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution)
}

func matchesVMClassNodeSelector(node *corev1.Node, vmClass *v1alpha2.VirtualMachineClass) (bool, error) {
nodeSelector := vmClass.Spec.NodeSelector
if len(nodeSelector.MatchLabels) > 0 {
if !labels.SelectorFromSet(nodeSelector.MatchLabels).Matches(labels.Set(node.Labels)) {
return false, nil
}
}
if len(nodeSelector.MatchExpressions) > 0 {
return corev1helpers.MatchNodeSelectorTerms(node, &corev1.NodeSelector{
NodeSelectorTerms: []corev1.NodeSelectorTerm{{
MatchExpressions: nodeSelector.MatchExpressions,
}},
})
}
return true, nil
}

func toleratesNodeTaints(node *corev1.Node, tolerations []corev1.Toleration) bool {
_, untolerated := corev1helpers.FindMatchingUntoleratedTaint(
node.Spec.Taints, tolerations,
func(t *corev1.Taint) bool {
return t.Effect == corev1.TaintEffectNoSchedule || t.Effect == corev1.TaintEffectNoExecute
},
)
return !untolerated
}

func CrossProductTerms(a, b []corev1.NodeSelectorTerm) []corev1.NodeSelectorTerm {
var result []corev1.NodeSelectorTerm
for _, termA := range a {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/*
Copyright 2026 Flant JSC

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 nodeaffinity_test

import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/deckhouse/virtualization-controller/pkg/common/nodeaffinity"
"github.com/deckhouse/virtualization/api/core/v1alpha2"
)

var _ = Describe("MatchesVMPlacement", func() {
makeNode := func(labels map[string]string, taints ...corev1.Taint) *corev1.Node {
return &corev1.Node{
ObjectMeta: metav1.ObjectMeta{Name: "node-1", Labels: labels},
Spec: corev1.NodeSpec{Taints: taints},
}
}

It("returns true for a node matching all rules", func() {
node := makeNode(map[string]string{"zone": "a"})
vm := &v1alpha2.VirtualMachine{Spec: v1alpha2.VirtualMachineSpec{
NodeSelector: map[string]string{"zone": "a"},
}}
vmClass := &v1alpha2.VirtualMachineClass{}
match, err := nodeaffinity.MatchesVMPlacement(node, vm, vmClass)
Expect(err).NotTo(HaveOccurred())
Expect(match).To(BeTrue())
})

It("returns false when node selector does not match", func() {
node := makeNode(map[string]string{"zone": "a"})
vm := &v1alpha2.VirtualMachine{Spec: v1alpha2.VirtualMachineSpec{
NodeSelector: map[string]string{"zone": "b"},
}}
match, err := nodeaffinity.MatchesVMPlacement(node, vm, &v1alpha2.VirtualMachineClass{})
Expect(err).NotTo(HaveOccurred())
Expect(match).To(BeFalse())
})

It("returns false when an untolerated NoSchedule taint is present", func() {
node := makeNode(nil, corev1.Taint{Key: "gpu", Value: "true", Effect: corev1.TaintEffectNoSchedule})
vm := &v1alpha2.VirtualMachine{}
match, err := nodeaffinity.MatchesVMPlacement(node, vm, &v1alpha2.VirtualMachineClass{})
Expect(err).NotTo(HaveOccurred())
Expect(match).To(BeFalse())
})

It("tolerates a taint when toleration is present", func() {
node := makeNode(nil, corev1.Taint{Key: "gpu", Value: "true", Effect: corev1.TaintEffectNoSchedule})
vm := &v1alpha2.VirtualMachine{Spec: v1alpha2.VirtualMachineSpec{
Tolerations: []corev1.Toleration{{Key: "gpu", Operator: corev1.TolerationOpEqual, Value: "true", Effect: corev1.TaintEffectNoSchedule}},
}}
match, err := nodeaffinity.MatchesVMPlacement(node, vm, &v1alpha2.VirtualMachineClass{})
Expect(err).NotTo(HaveOccurred())
Expect(match).To(BeTrue())
})

It("matches VM nodeAffinity", func() {
node := makeNode(map[string]string{"zone": "a"})
vm := &v1alpha2.VirtualMachine{Spec: v1alpha2.VirtualMachineSpec{
Affinity: &v1alpha2.VMAffinity{
NodeAffinity: &corev1.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
NodeSelectorTerms: []corev1.NodeSelectorTerm{{
MatchExpressions: []corev1.NodeSelectorRequirement{{
Key: "zone", Operator: corev1.NodeSelectorOpIn, Values: []string{"a"},
}},
}},
},
},
},
}}
match, err := nodeaffinity.MatchesVMPlacement(node, vm, &v1alpha2.VirtualMachineClass{})
Expect(err).NotTo(HaveOccurred())
Expect(match).To(BeTrue())
})

It("rejects a node failing VM nodeAffinity", func() {
node := makeNode(map[string]string{"zone": "b"})
vm := &v1alpha2.VirtualMachine{Spec: v1alpha2.VirtualMachineSpec{
Affinity: &v1alpha2.VMAffinity{
NodeAffinity: &corev1.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
NodeSelectorTerms: []corev1.NodeSelectorTerm{{
MatchExpressions: []corev1.NodeSelectorRequirement{{
Key: "zone", Operator: corev1.NodeSelectorOpIn, Values: []string{"a"},
}},
}},
},
},
},
}}
match, err := nodeaffinity.MatchesVMPlacement(node, vm, &v1alpha2.VirtualMachineClass{})
Expect(err).NotTo(HaveOccurred())
Expect(match).To(BeFalse())
})

It("returns error when VM nodeAffinity has a malformed operator", func() {
node := makeNode(map[string]string{"zone": "a"})
vm := &v1alpha2.VirtualMachine{Spec: v1alpha2.VirtualMachineSpec{
Affinity: &v1alpha2.VMAffinity{
NodeAffinity: &corev1.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
NodeSelectorTerms: []corev1.NodeSelectorTerm{{
MatchExpressions: []corev1.NodeSelectorRequirement{{
Key: "zone", Operator: "Bogus", Values: []string{"a"},
}},
}},
},
},
},
}}
_, err := nodeaffinity.MatchesVMPlacement(node, vm, &v1alpha2.VirtualMachineClass{})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("VM affinity"))
})

It("matches VM class node selector (MatchLabels)", func() {
node := makeNode(map[string]string{"role": "worker"})
vmClass := &v1alpha2.VirtualMachineClass{Spec: v1alpha2.VirtualMachineClassSpec{
NodeSelector: v1alpha2.NodeSelector{
MatchLabels: map[string]string{"role": "worker"},
},
}}
match, err := nodeaffinity.MatchesVMPlacement(node, &v1alpha2.VirtualMachine{}, vmClass)
Expect(err).NotTo(HaveOccurred())
Expect(match).To(BeTrue())
})

It("rejects node not matching VM class MatchLabels", func() {
node := makeNode(map[string]string{"role": "controlplane"})
vmClass := &v1alpha2.VirtualMachineClass{Spec: v1alpha2.VirtualMachineClassSpec{
NodeSelector: v1alpha2.NodeSelector{
MatchLabels: map[string]string{"role": "worker"},
},
}}
match, err := nodeaffinity.MatchesVMPlacement(node, &v1alpha2.VirtualMachine{}, vmClass)
Expect(err).NotTo(HaveOccurred())
Expect(match).To(BeFalse())
})

It("returns error when VM class nodeSelector has a malformed operator", func() {
node := makeNode(map[string]string{"zone": "a"})
vmClass := &v1alpha2.VirtualMachineClass{Spec: v1alpha2.VirtualMachineClassSpec{
NodeSelector: v1alpha2.NodeSelector{
MatchExpressions: []corev1.NodeSelectorRequirement{{
Key: "zone", Operator: "Bogus", Values: []string{"a"},
}},
},
}}
_, err := nodeaffinity.MatchesVMPlacement(node, &v1alpha2.VirtualMachine{}, vmClass)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("VM class node selector"))
})
})

var _ = Describe("IntersectTerms", func() {
It("returns nil for empty input", func() {
Expect(nodeaffinity.IntersectTerms(nil)).To(BeNil())
Expect(nodeaffinity.IntersectTerms([][]corev1.NodeSelectorTerm{})).To(BeNil())
})

It("returns the only term set unchanged", func() {
terms := []corev1.NodeSelectorTerm{{
MatchExpressions: []corev1.NodeSelectorRequirement{{Key: "a", Operator: corev1.NodeSelectorOpIn, Values: []string{"1"}}},
}}
result := nodeaffinity.IntersectTerms([][]corev1.NodeSelectorTerm{terms})
Expect(result).To(Equal(terms))
})

It("computes the cross product of two term sets", func() {
a := []corev1.NodeSelectorTerm{{
MatchExpressions: []corev1.NodeSelectorRequirement{{Key: "a", Operator: corev1.NodeSelectorOpIn, Values: []string{"1"}}},
}}
b := []corev1.NodeSelectorTerm{{
MatchExpressions: []corev1.NodeSelectorRequirement{{Key: "b", Operator: corev1.NodeSelectorOpIn, Values: []string{"2"}}},
}}
result := nodeaffinity.IntersectTerms([][]corev1.NodeSelectorTerm{a, b})
Expect(result).To(HaveLen(1))
Expect(result[0].MatchExpressions).To(HaveLen(2))
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
Copyright 2026 Flant JSC

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 nodeaffinity_test

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestNodeAffinity(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "NodeAffinity Suite")
}
Loading
Loading