Skip to content

Commit 7ef2217

Browse files
chore(vm): add system migration policy (#2236)
* chore(vm): add system migration policy (#2156) Add a system-level live migration policy override sourced from ModuleConfig/virtualization annotation virtualization.deckhouse.io/system-migration-policy. Signed-off-by: Dmitry Lopatin <dmitry.lopatin@flant.com> Signed-off-by: Dmitry Lopatin <93423466+LopatinDmitr@users.noreply.github.com> Co-authored-by: Ivan Mikheykin <ivan.mikheykin@flant.com>
1 parent ddd3150 commit 7ef2217

7 files changed

Lines changed: 242 additions & 3 deletions

File tree

images/virtualization-artifact/cmd/virtualization-controller/main.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import (
6969
workloadupdater "github.com/deckhouse/virtualization-controller/pkg/controller/workload-updater"
7070
"github.com/deckhouse/virtualization-controller/pkg/crd"
7171
"github.com/deckhouse/virtualization-controller/pkg/featuregates"
72+
livemigrationcfg "github.com/deckhouse/virtualization-controller/pkg/livemigration"
7273
"github.com/deckhouse/virtualization-controller/pkg/logger"
7374
"github.com/deckhouse/virtualization-controller/pkg/migration"
7475
"github.com/deckhouse/virtualization-controller/pkg/version"
@@ -311,6 +312,18 @@ func main() {
311312
log.Error(err.Error())
312313
os.Exit(1)
313314
}
315+
316+
systemMigrationPolicy := livemigrationcfg.GetSystemMigrationPolicyAnnotation(ctx, preManagerClient)
317+
switch {
318+
case systemMigrationPolicy == "":
319+
appconfig.ResetSystemMigrationPolicyOverride()
320+
case appconfig.SetSystemMigrationPolicyOverride(systemMigrationPolicy):
321+
log.Info("System migration policy override is set", "value", systemMigrationPolicy)
322+
default:
323+
appconfig.ResetSystemMigrationPolicyOverride()
324+
log.Warn("System migration policy override has invalid value, override disabled", "value", systemMigrationPolicy)
325+
}
326+
314327
mCtrl, err := migration.NewController(preManagerClient, log)
315328
if err != nil {
316329
log.Error(err.Error())

images/virtualization-artifact/pkg/config/load_live_migration_settings.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,40 @@ import (
2525
const (
2626
DefaultLiveMigrationPolicy = v1alpha2.PreferSafeMigrationPolicy
2727
)
28+
29+
var systemMigrationPolicyOverride v1alpha2.LiveMigrationPolicy
30+
31+
func SetSystemMigrationPolicyOverride(rawPolicy string) bool {
32+
policy := v1alpha2.LiveMigrationPolicy(rawPolicy)
33+
if !isValidLiveMigrationPolicy(policy) {
34+
systemMigrationPolicyOverride = ""
35+
return false
36+
}
37+
systemMigrationPolicyOverride = policy
38+
return true
39+
}
40+
41+
func GetSystemMigrationPolicyOverride() (v1alpha2.LiveMigrationPolicy, bool) {
42+
if systemMigrationPolicyOverride == "" {
43+
return "", false
44+
}
45+
return systemMigrationPolicyOverride, true
46+
}
47+
48+
func ResetSystemMigrationPolicyOverride() {
49+
systemMigrationPolicyOverride = ""
50+
}
51+
52+
func isValidLiveMigrationPolicy(policy v1alpha2.LiveMigrationPolicy) bool {
53+
switch policy {
54+
case v1alpha2.ManualMigrationPolicy,
55+
v1alpha2.NeverMigrationPolicy,
56+
v1alpha2.AlwaysSafeMigrationPolicy,
57+
v1alpha2.PreferSafeMigrationPolicy,
58+
v1alpha2.AlwaysForcedMigrationPolicy,
59+
v1alpha2.PreferForcedMigrationPolicy:
60+
return true
61+
default:
62+
return false
63+
}
64+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
Copyright 2026 Flant JSC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package config
18+
19+
import (
20+
"testing"
21+
22+
. "github.com/onsi/ginkgo/v2"
23+
. "github.com/onsi/gomega"
24+
25+
"github.com/deckhouse/virtualization/api/core/v1alpha2"
26+
)
27+
28+
var _ = Describe("SystemMigrationPolicyOverride", func() {
29+
BeforeEach(func() {
30+
ResetSystemMigrationPolicyOverride()
31+
})
32+
33+
AfterEach(func() {
34+
ResetSystemMigrationPolicyOverride()
35+
})
36+
37+
DescribeTable("accepts valid values",
38+
func(policy v1alpha2.LiveMigrationPolicy) {
39+
ok := SetSystemMigrationPolicyOverride(string(policy))
40+
Expect(ok).To(BeTrue())
41+
42+
actual, exists := GetSystemMigrationPolicyOverride()
43+
Expect(exists).To(BeTrue())
44+
Expect(actual).To(Equal(policy))
45+
},
46+
Entry("Manual", v1alpha2.ManualMigrationPolicy),
47+
Entry("Never", v1alpha2.NeverMigrationPolicy),
48+
Entry("AlwaysSafe", v1alpha2.AlwaysSafeMigrationPolicy),
49+
Entry("PreferSafe", v1alpha2.PreferSafeMigrationPolicy),
50+
Entry("AlwaysForced", v1alpha2.AlwaysForcedMigrationPolicy),
51+
Entry("PreferForced", v1alpha2.PreferForcedMigrationPolicy),
52+
)
53+
54+
It("rejects invalid value", func() {
55+
ok := SetSystemMigrationPolicyOverride("invalid")
56+
Expect(ok).To(BeFalse())
57+
58+
actual, exists := GetSystemMigrationPolicyOverride()
59+
Expect(exists).To(BeFalse())
60+
Expect(actual).To(Equal(v1alpha2.LiveMigrationPolicy("")))
61+
})
62+
63+
It("reset clears override", func() {
64+
ok := SetSystemMigrationPolicyOverride(string(v1alpha2.PreferSafeMigrationPolicy))
65+
Expect(ok).To(BeTrue())
66+
67+
ResetSystemMigrationPolicyOverride()
68+
69+
actual, exists := GetSystemMigrationPolicyOverride()
70+
Expect(exists).To(BeFalse())
71+
Expect(actual).To(Equal(v1alpha2.LiveMigrationPolicy("")))
72+
})
73+
})
74+
75+
func TestSystemMigrationPolicyOverride(t *testing.T) {
76+
RegisterFailHandler(Fail)
77+
RunSpecs(t, "SystemMigrationPolicyOverride Suite")
78+
}

images/virtualization-artifact/pkg/livemigration/policy.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,18 @@ func AutoConvergeForPolicy(policy v1alpha2.LiveMigrationPolicy) (autoConverge *b
3939
// Also, autoConverge value may be overridden from VMOP.
4040
func CalculateEffectivePolicy(vm v1alpha2.VirtualMachine, vmop *v1alpha2.VirtualMachineOperation) (effectivePolicy v1alpha2.LiveMigrationPolicy, autoConverge bool, err error) {
4141
effectivePolicy = config.DefaultLiveMigrationPolicy
42+
overridePolicy, hasSystemOverride := config.GetSystemMigrationPolicyOverride()
4243

43-
if vm.Spec.LiveMigrationPolicy != "" {
44+
if hasSystemOverride {
45+
effectivePolicy = overridePolicy
46+
} else if vm.Spec.LiveMigrationPolicy != "" {
4447
effectivePolicy = vm.Spec.LiveMigrationPolicy
4548
}
4649

4750
autoConvergePtr := AutoConvergeForPolicy(effectivePolicy)
4851

49-
// Override autoConverge value.
50-
if vmop != nil {
52+
// VMOP force may override autoConverge only when system override is not set.
53+
if vmop != nil && !hasSystemOverride {
5154
switch effectivePolicy {
5255
case v1alpha2.PreferSafeMigrationPolicy, v1alpha2.PreferForcedMigrationPolicy:
5356
if vmop.Spec.Force != nil {

images/virtualization-artifact/pkg/livemigration/policy_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,19 @@ import (
2424
"github.com/stretchr/testify/require"
2525
"k8s.io/utils/ptr"
2626

27+
"github.com/deckhouse/virtualization-controller/pkg/config"
2728
"github.com/deckhouse/virtualization/api/core/v1alpha2"
2829
)
2930

3031
var _ = Describe("CalculateEffectivePolicy", func() {
32+
BeforeEach(func() {
33+
config.ResetSystemMigrationPolicyOverride()
34+
})
35+
36+
AfterEach(func() {
37+
config.ResetSystemMigrationPolicyOverride()
38+
})
39+
3140
DescribeTable("effective policy and autoConverge value",
3241
func(
3342
vmPolicy v1alpha2.LiveMigrationPolicy,
@@ -80,6 +89,53 @@ var _ = Describe("CalculateEffectivePolicy", func() {
8089

8190
Entry("No VM policy with no vmop", v1alpha2.LiveMigrationPolicy(""), nil, v1alpha2.PreferSafeMigrationPolicy, false, false),
8291
)
92+
93+
DescribeTable("system override takes precedence and ignores force",
94+
func(
95+
systemPolicy v1alpha2.LiveMigrationPolicy,
96+
vmPolicy v1alpha2.LiveMigrationPolicy,
97+
vmopForce *bool,
98+
expectedPolicy v1alpha2.LiveMigrationPolicy,
99+
expectedResult bool,
100+
expectError bool,
101+
) {
102+
ok := config.SetSystemMigrationPolicyOverride(string(systemPolicy))
103+
require.True(GinkgoT(), ok)
104+
105+
vm := v1alpha2.VirtualMachine{}
106+
if vmPolicy != "" {
107+
vm.Spec.LiveMigrationPolicy = vmPolicy
108+
}
109+
110+
var vmop *v1alpha2.VirtualMachineOperation
111+
if vmopForce != nil {
112+
vmop = &v1alpha2.VirtualMachineOperation{
113+
Spec: v1alpha2.VirtualMachineOperationSpec{
114+
Force: vmopForce,
115+
},
116+
}
117+
}
118+
119+
policy, result, err := CalculateEffectivePolicy(vm, vmop)
120+
121+
if expectError {
122+
require.Error(GinkgoT(), err)
123+
} else {
124+
require.NoError(GinkgoT(), err)
125+
}
126+
127+
require.Equal(GinkgoT(), expectedPolicy, policy)
128+
require.Equal(GinkgoT(), expectedResult, result)
129+
},
130+
131+
Entry("system override AlwaysForced overrides VM PreferSafe", v1alpha2.AlwaysForcedMigrationPolicy, v1alpha2.PreferSafeMigrationPolicy, nil, v1alpha2.AlwaysForcedMigrationPolicy, true, false),
132+
Entry("system override AlwaysSafe overrides VM PreferForced", v1alpha2.AlwaysSafeMigrationPolicy, v1alpha2.PreferForcedMigrationPolicy, nil, v1alpha2.AlwaysSafeMigrationPolicy, false, false),
133+
Entry("system override Never overrides default policy", v1alpha2.NeverMigrationPolicy, v1alpha2.LiveMigrationPolicy(""), nil, v1alpha2.NeverMigrationPolicy, false, false),
134+
Entry("force=true is ignored when system override PreferSafe is set", v1alpha2.PreferSafeMigrationPolicy, v1alpha2.PreferSafeMigrationPolicy, ptr.To(true), v1alpha2.PreferSafeMigrationPolicy, false, false),
135+
Entry("force=false is ignored when system override PreferForced is set", v1alpha2.PreferForcedMigrationPolicy, v1alpha2.PreferForcedMigrationPolicy, ptr.To(false), v1alpha2.PreferForcedMigrationPolicy, true, false),
136+
Entry("AlwaysSafe with force=true does not error when system override is set", v1alpha2.AlwaysSafeMigrationPolicy, v1alpha2.PreferForcedMigrationPolicy, ptr.To(true), v1alpha2.AlwaysSafeMigrationPolicy, false, false),
137+
Entry("AlwaysForced with force=false does not error when system override is set", v1alpha2.AlwaysForcedMigrationPolicy, v1alpha2.PreferSafeMigrationPolicy, ptr.To(false), v1alpha2.AlwaysForcedMigrationPolicy, true, false),
138+
)
83139
})
84140

85141
func TestCalculateEffectivePolicy(t *testing.T) {
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
Copyright 2026 Flant JSC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package livemigration
18+
19+
import (
20+
"context"
21+
"log/slog"
22+
23+
apierrors "k8s.io/apimachinery/pkg/api/errors"
24+
"sigs.k8s.io/controller-runtime/pkg/client"
25+
26+
mcapi "github.com/deckhouse/virtualization-controller/pkg/controller/moduleconfig/api"
27+
"github.com/deckhouse/virtualization-controller/pkg/logger"
28+
)
29+
30+
const (
31+
moduleConfigName = "virtualization"
32+
systemMigrationPolicyAnnotation = "virtualization.deckhouse.io/system-migration-policy"
33+
)
34+
35+
func GetSystemMigrationPolicyAnnotation(ctx context.Context, kubeClient client.Client) string {
36+
moduleConfig := &mcapi.ModuleConfig{}
37+
err := kubeClient.Get(ctx, client.ObjectKey{Name: moduleConfigName}, moduleConfig)
38+
if err != nil {
39+
if !apierrors.IsNotFound(err) {
40+
slog.Default().Error("failed to get ModuleConfig virtualization", logger.SlogErr(err))
41+
}
42+
return ""
43+
}
44+
45+
return moduleConfig.GetAnnotations()[systemMigrationPolicyAnnotation]
46+
}

templates/virtualization-controller/rbac-for-us.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@ rules:
111111
- patch
112112
- list
113113
- watch
114+
- apiGroups:
115+
- deckhouse.io
116+
resources:
117+
- moduleconfigs
118+
verbs:
119+
- get
114120
- apiGroups:
115121
- cdi.internal.virtualization.deckhouse.io
116122
resources:

0 commit comments

Comments
 (0)