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
1 change: 1 addition & 0 deletions images/hooks/cmd/virtualization-module-hooks/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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))))
})
})
2 changes: 2 additions & 0 deletions openapi/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ properties:
type: string
parallelMigrationsPerCluster:
type: integer
parallelOutboundMigrationsPerNode:
type: integer
moduleConfig:
type: object
additionalProperties: true
Expand Down
2 changes: 1 addition & 1 deletion templates/kubevirt/kubevirt.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions tools/kubeconform/fixtures/module-values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ virtualization:
virtConfig:
phase: Deployed
parallelMigrationsPerCluster: 2
parallelOutboundMigrationsPerNode: 10
virtHandler:
nodeCount: 1
logLevel: debug
Expand Down
Loading