diff --git a/images/hooks/cmd/virtualization-module-hooks/register.go b/images/hooks/cmd/virtualization-module-hooks/register.go index 8f6cebea9e..d8328a1533 100644 --- a/images/hooks/cmd/virtualization-module-hooks/register.go +++ b/images/hooks/cmd/virtualization-module-hooks/register.go @@ -26,6 +26,7 @@ import ( _ "hooks/pkg/hooks/generate-secret-for-dvcr" _ "hooks/pkg/hooks/install-vmclass-generic" _ "hooks/pkg/hooks/migrate-virthandler-kvm-node-labels" + _ "hooks/pkg/hooks/parallel-outbound-migrations-per-node" _ "hooks/pkg/hooks/tls-certificates-api" _ "hooks/pkg/hooks/tls-certificates-api-proxy" _ "hooks/pkg/hooks/tls-certificates-controller" diff --git a/images/hooks/pkg/hooks/parallel-outbound-migrations-per-node/hook.go b/images/hooks/pkg/hooks/parallel-outbound-migrations-per-node/hook.go new file mode 100644 index 0000000000..7a3b7f4774 --- /dev/null +++ b/images/hooks/pkg/hooks/parallel-outbound-migrations-per-node/hook.go @@ -0,0 +1,77 @@ +package parallel_outbound_migrations_per_node + +import ( + "context" + "fmt" + "strconv" + + "github.com/deckhouse/module-sdk/pkg" + "github.com/deckhouse/module-sdk/pkg/registry" + "k8s.io/utils/ptr" + + "hooks/pkg/settings" +) + +const ( + snapshotModuleConfig = "module-config" + moduleConfigJQFilter = `.metadata.annotations` + migrationsPerNodeAnnotationKey = "virtualization.deckhouse.io/parallel-outbound-migrations-per-node" + migrationsPerNodeValuesPath = "virtualization.internal.virtConfig.parallelOutboundMigrationsPerNode" + defaultMigrationsPerNode = 1 +) + +var _ = registry.RegisterFunc(config, reconcile) + +var config = &pkg.HookConfig{ + OnBeforeHelm: &pkg.OrderedConfig{Order: 10}, + Kubernetes: []pkg.KubernetesConfig{ + { + Name: snapshotModuleConfig, + APIVersion: "deckhouse.io/v1alpha1", + Kind: "ModuleConfig", + NameSelector: &pkg.NameSelector{ + MatchNames: []string{settings.ModuleName}, + }, + ExecuteHookOnSynchronization: ptr.To(true), + ExecuteHookOnEvents: ptr.To(true), + JqFilter: moduleConfigJQFilter, + }, + }, + + Queue: fmt.Sprintf("modules/%s", settings.ModuleName), +} + +func reconcile(_ context.Context, input *pkg.HookInput) error { + parallelOutboundMigrationsPerNode, err := parallelOutboundMigrationsPerNodeFromSnapshot(input) + if err != nil { + return err + } + current := int(input.Values.Get(migrationsPerNodeValuesPath).Int()) + if current != parallelOutboundMigrationsPerNode { + input.Values.Set(migrationsPerNodeValuesPath, parallelOutboundMigrationsPerNode) + } + return nil +} + +func parallelOutboundMigrationsPerNodeFromSnapshot(input *pkg.HookInput) (int, error) { + snap := input.Snapshots.Get(snapshotModuleConfig) + if len(snap) < 1 { + return -1, fmt.Errorf("moduleConfig is missing, something wrong with Deckhouse configuration") + } + + var annos map[string]string + err := snap[0].UnmarshalTo(&annos) + if err != nil { + return -1, fmt.Errorf("failed to unmarshal moduleConfig annotations: %w", err) + } + + if val, ok := annos[migrationsPerNodeAnnotationKey]; ok { + valInt, err := strconv.Atoi(val) + if err != nil { + return -1, fmt.Errorf("failed to parse %q annotation: %w", migrationsPerNodeAnnotationKey, err) + } + return valInt, nil + } + + return defaultMigrationsPerNode, nil +} diff --git a/images/hooks/pkg/hooks/parallel-outbound-migrations-per-node/hook_test.go b/images/hooks/pkg/hooks/parallel-outbound-migrations-per-node/hook_test.go new file mode 100644 index 0000000000..4f25369ccc --- /dev/null +++ b/images/hooks/pkg/hooks/parallel-outbound-migrations-per-node/hook_test.go @@ -0,0 +1,126 @@ +/* +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 parallel_outbound_migrations_per_node + +import ( + "context" + "fmt" + "maps" + "testing" + + "github.com/deckhouse/deckhouse/pkg/log" + "github.com/deckhouse/module-sdk/pkg" + "github.com/deckhouse/module-sdk/testing/mock" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/tidwall/gjson" +) + +func TestParallelOutboundMigrationsPerNode(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "ParallelOutboundMigrationsPerNode Suite") +} + +var _ = Describe("ParallelOutboundMigrationsPerNode", func() { + var ( + dc *mock.DependencyContainerMock + snapshots *mock.SnapshotsMock + values *mock.OutputPatchableValuesCollectorMock + ) + + setSnapshots := func(snaps ...pkg.Snapshot) { + snapshots.GetMock.When(snapshotModuleConfig).Then(snaps) + } + + newSnapshot := func(annos map[string]string) pkg.Snapshot { + return mock.NewSnapshotMock(GinkgoT()).UnmarshalToMock.Set(func(v any) (err error) { + data, ok := v.(*map[string]string) + Expect(ok).To(BeTrue()) + *data = make(map[string]string) + maps.Copy(*data, annos) + return nil + }) + } + + newInput := func() *pkg.HookInput { + return &pkg.HookInput{ + Snapshots: snapshots, + Values: values, + DC: dc, + Logger: log.NewNop(), + } + } + + BeforeEach(func() { + dc = mock.NewDependencyContainerMock(GinkgoT()) + snapshots = mock.NewSnapshotsMock(GinkgoT()) + values = mock.NewPatchableValuesCollectorMock(GinkgoT()) + }) + + AfterEach(func() { + dc = nil + snapshots = nil + values = nil + }) + + It("Should set parallel outbound migrations per node", func() { + setSnapshots(newSnapshot(map[string]string{ + migrationsPerNodeAnnotationKey: "5", + })) + + values.GetMock.When(migrationsPerNodeValuesPath).Then(gjson.Result{Type: gjson.Number, Num: 1}) + + values.SetMock.Set(func(path string, v any) { + value, ok := v.(int) + Expect(ok).To(BeTrue()) + Expect(value).To(Equal(5)) + }) + + Expect(reconcile(context.Background(), newInput())).To(Succeed()) + }) + + It("Should set default parallel outbound migrations per node", func() { + setSnapshots(newSnapshot(map[string]string{})) + + values.GetMock.When(migrationsPerNodeValuesPath).Then(gjson.Result{Type: gjson.Number, Num: 5}) + + values.SetMock.Set(func(path string, v any) { + value, ok := v.(int) + Expect(ok).To(BeTrue()) + Expect(value).To(Equal(defaultMigrationsPerNode)) + }) + + Expect(reconcile(context.Background(), newInput())).To(Succeed()) + }) + + It("Should don't set parallel outbound migrations per node if it's already set", func() { + setSnapshots(newSnapshot(map[string]string{ + migrationsPerNodeAnnotationKey: "5", + })) + + values.GetMock.When(migrationsPerNodeValuesPath).Then(gjson.Result{Type: gjson.Number, Num: 5}) + Expect(reconcile(context.Background(), newInput())).To(Succeed()) + }) + + It("Should finish with error because annotations is wrong", func() { + setSnapshots(newSnapshot(map[string]string{ + migrationsPerNodeAnnotationKey: "wrong", + })) + + Expect(reconcile(context.Background(), newInput())).To(MatchError(ContainSubstring(fmt.Sprintf("failed to parse %q annotation:", migrationsPerNodeAnnotationKey)))) + }) +}) diff --git a/openapi/values.yaml b/openapi/values.yaml index 1b20082afe..8bb087ea54 100644 --- a/openapi/values.yaml +++ b/openapi/values.yaml @@ -133,6 +133,8 @@ properties: type: string parallelMigrationsPerCluster: type: integer + parallelOutboundMigrationsPerNode: + type: integer moduleConfig: type: object additionalProperties: true diff --git a/templates/kubevirt/kubevirt.yaml b/templates/kubevirt/kubevirt.yaml index 81b71c9949..01c1d07708 100644 --- a/templates/kubevirt/kubevirt.yaml +++ b/templates/kubevirt/kubevirt.yaml @@ -32,7 +32,7 @@ spec: bandwidthPerMigration: 640Mi completionTimeoutPerGiB: 800 parallelMigrationsPerCluster: {{ include "kubevirt.parallel_migrations_per_cluster" . }} - parallelOutboundMigrationsPerNode: 1 + parallelOutboundMigrationsPerNode: {{ .Values.virtualization.internal.virtConfig.parallelOutboundMigrationsPerNode }} progressTimeout: 150 smbios: manufacturer: Flant diff --git a/tools/kubeconform/fixtures/module-values.yaml b/tools/kubeconform/fixtures/module-values.yaml index 88282bddb2..05f6329db0 100644 --- a/tools/kubeconform/fixtures/module-values.yaml +++ b/tools/kubeconform/fixtures/module-values.yaml @@ -399,6 +399,7 @@ virtualization: virtConfig: phase: Deployed parallelMigrationsPerCluster: 2 + parallelOutboundMigrationsPerNode: 10 virtHandler: nodeCount: 1 logLevel: debug