From 0e9c792f636cf7da7b491c6c3b3462d8fdc74adc Mon Sep 17 00:00:00 2001 From: Pavel Tishkov Date: Wed, 15 Oct 2025 12:58:29 +0300 Subject: [PATCH 01/17] fix(module): make vmclass generic editable --- .../virtualization-module-hooks/register.go | 2 + .../pkg/hooks/create-generic-vmclass/hook.go | 179 +++++++++++++++ .../hooks/create-generic-vmclass/hook_test.go | 143 ++++++++++++ .../pkg/hooks/update-module-state/hook.go | 159 +++++++++++++ .../hooks/update-module-state/hook_test.go | 213 ++++++++++++++++++ .../vmclasses-default.yaml | 34 --- 6 files changed, 696 insertions(+), 34 deletions(-) create mode 100644 images/hooks/pkg/hooks/create-generic-vmclass/hook.go create mode 100644 images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go create mode 100644 images/hooks/pkg/hooks/update-module-state/hook.go create mode 100644 images/hooks/pkg/hooks/update-module-state/hook_test.go delete mode 100644 templates/virtualization-controller/vmclasses-default.yaml diff --git a/images/hooks/cmd/virtualization-module-hooks/register.go b/images/hooks/cmd/virtualization-module-hooks/register.go index bc0e1dc2a4..e906c1b981 100644 --- a/images/hooks/cmd/virtualization-module-hooks/register.go +++ b/images/hooks/cmd/virtualization-module-hooks/register.go @@ -18,6 +18,7 @@ package main import ( _ "hooks/pkg/hooks/ca-discovery" _ "hooks/pkg/hooks/copy-custom-certificate" + _ "hooks/pkg/hooks/create-generic-vmclass" _ "hooks/pkg/hooks/discovery-clusterip-service-for-dvcr" _ "hooks/pkg/hooks/discovery-workload-nodes" _ "hooks/pkg/hooks/drop-openshift-labels" @@ -29,5 +30,6 @@ import ( _ "hooks/pkg/hooks/tls-certificates-api-proxy" _ "hooks/pkg/hooks/tls-certificates-controller" _ "hooks/pkg/hooks/tls-certificates-dvcr" + _ "hooks/pkg/hooks/update-module-state" _ "hooks/pkg/hooks/validate-module-config" ) diff --git a/images/hooks/pkg/hooks/create-generic-vmclass/hook.go b/images/hooks/pkg/hooks/create-generic-vmclass/hook.go new file mode 100644 index 0000000000..70d1be162d --- /dev/null +++ b/images/hooks/pkg/hooks/create-generic-vmclass/hook.go @@ -0,0 +1,179 @@ +/* +Copyright 2025 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 create_generic_vmclass + +import ( + "context" + "encoding/base64" + "fmt" + + "hooks/pkg/settings" + + "github.com/deckhouse/virtualization/api/core" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + + "github.com/deckhouse/module-sdk/pkg" + "github.com/deckhouse/module-sdk/pkg/registry" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" +) + +const ( + createGenericVMClassHookName = "Create generic VirtualMachineClass" + moduleStateSecretSnapshot = "module-state-secret" + vmClassSnapshot = "vmclass-generic" + + moduleStateSecretName = "module-state" + genericVMClassName = "generic" + + apiVersion = core.GroupName + "/" + v1alpha2.Version +) + +var _ = registry.RegisterFunc(config, Reconcile) + +var config = &pkg.HookConfig{ + OnBeforeHelm: &pkg.OrderedConfig{Order: 5}, + Kubernetes: []pkg.KubernetesConfig{ + { + Name: moduleStateSecretSnapshot, + APIVersion: "v1", + Kind: "Secret", + JqFilter: `.data`, + NameSelector: &pkg.NameSelector{ + MatchNames: []string{moduleStateSecretName}, + }, + NamespaceSelector: &pkg.NamespaceSelector{ + NameSelector: &pkg.NameSelector{ + MatchNames: []string{settings.ModuleNamespace}, + }, + }, + ExecuteHookOnSynchronization: ptr.To(false), + }, + { + Name: vmClassSnapshot, + APIVersion: apiVersion, + Kind: v1alpha2.VirtualMachineClassKind, + JqFilter: `.metadata.name`, + NameSelector: &pkg.NameSelector{ + MatchNames: []string{genericVMClassName}, + }, + ExecuteHookOnSynchronization: ptr.To(false), + }, + }, + + Queue: fmt.Sprintf("modules/%s", settings.ModuleName), +} + +func Reconcile(_ context.Context, input *pkg.HookInput) error { + // Проверяем, есть ли запись о том, что generic vmclass был создан ранее + moduleStateSecrets := input.Snapshots.Get(moduleStateSecretSnapshot) + vmClasses := input.Snapshots.Get(vmClassSnapshot) + + // Если секрет module-state существует и содержит информацию о создании generic vmclass + shouldCreateVMClass := false + if len(moduleStateSecrets) > 0 { + moduleStateData := make(map[string]interface{}) + if err := moduleStateSecrets[0].UnmarshalTo(&moduleStateData); err == nil { + if genericCreatedEncoded, exists := moduleStateData["generic-vmclass-created"]; exists { + if encodedStr, ok := genericCreatedEncoded.(string); ok { + // Декодируем base64 строку + if decodedBytes, err := base64.StdEncoding.DecodeString(encodedStr); err == nil { + if string(decodedBytes) == "true" { + shouldCreateVMClass = true + input.Logger.Info("Found record in module-state that generic vmclass was created previously") + } + } + } + } + } + } + + // Проверяем, существует ли generic vmclass + vmClassExists := len(vmClasses) > 0 + + // Создаем vmclass generic если: + // 1. В секрете module-state есть запись о том, что он был создан ранее + // 2. И vmclass generic отсутствует + if shouldCreateVMClass && !vmClassExists { + input.Logger.Info("Creating generic VirtualMachineClass as it was previously created but is now missing") + + vmClass := &v1alpha2.VirtualMachineClass{ + TypeMeta: metav1.TypeMeta{ + APIVersion: apiVersion, + Kind: v1alpha2.VirtualMachineClassKind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: genericVMClassName, + Labels: map[string]string{ + "module": settings.ModuleName, + }, + Annotations: map[string]string{ + "helm.sh/resource-policy": "keep", + }, + }, + Spec: v1alpha2.VirtualMachineClassSpec{ + CPU: v1alpha2.CPU{ + Type: v1alpha2.CPUTypeModel, + Model: "Nehalem", + }, + SizingPolicies: []v1alpha2.SizingPolicy{ + { + Cores: &v1alpha2.SizingPolicyCores{ + Min: 1, + Max: 4, + }, + DedicatedCores: []bool{false}, + CoreFractions: []v1alpha2.CoreFractionValue{5, 10, 20, 50, 100}, + }, + { + Cores: &v1alpha2.SizingPolicyCores{ + Min: 5, + Max: 8, + }, + DedicatedCores: []bool{false}, + CoreFractions: []v1alpha2.CoreFractionValue{20, 50, 100}, + }, + { + Cores: &v1alpha2.SizingPolicyCores{ + Min: 9, + Max: 16, + }, + DedicatedCores: []bool{true, false}, + CoreFractions: []v1alpha2.CoreFractionValue{50, 100}, + }, + { + Cores: &v1alpha2.SizingPolicyCores{ + Min: 17, + Max: 1024, + }, + DedicatedCores: []bool{true, false}, + CoreFractions: []v1alpha2.CoreFractionValue{100}, + }, + }, + }, + } + + input.PatchCollector.Create(vmClass) + input.Logger.Info("Generic VirtualMachineClass creation requested") + } else if shouldCreateVMClass && vmClassExists { + input.Logger.Info("Generic VirtualMachineClass already exists, no action needed") + } else if !shouldCreateVMClass && !vmClassExists { + input.Logger.Info("No record of generic vmclass creation in module-state, skipping creation") + } + + return nil +} diff --git a/images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go b/images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go new file mode 100644 index 0000000000..d4dbd78feb --- /dev/null +++ b/images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go @@ -0,0 +1,143 @@ +/* +Copyright 2025 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 create_generic_vmclass + +import ( + "context" + "encoding/base64" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/deckhouse/deckhouse/pkg/log" + "github.com/deckhouse/module-sdk/pkg" + "github.com/deckhouse/module-sdk/testing/mock" + "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +func TestCreateGenericVMClass(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Create Generic VMClass Suite") +} + +var _ = Describe("Create Generic VMClass hook", func() { + var ( + snapshots *mock.SnapshotsMock + patchCollector *mock.PatchCollectorMock + ) + + newInput := func() *pkg.HookInput { + return &pkg.HookInput{ + Snapshots: snapshots, + PatchCollector: patchCollector, + Logger: log.NewNop(), + } + } + + BeforeEach(func() { + snapshots = mock.NewSnapshotsMock(GinkgoT()) + patchCollector = mock.NewPatchCollectorMock(GinkgoT()) + }) + + AfterEach(func() { + snapshots = nil + patchCollector = nil + }) + + Context("when module-state secret exists with generic-vmclass-created=true", func() { + BeforeEach(func() { + moduleStateData := map[string]interface{}{ + "generic-vmclass-created": base64.StdEncoding.EncodeToString([]byte("true")), + } + + snapshots.GetMock.When(moduleStateSecretSnapshot).Then([]pkg.Snapshot{ + mock.NewSnapshotMock(GinkgoT()).UnmarshalToMock.Set(func(v any) error { + data, ok := v.(*map[string]interface{}) + Expect(ok).To(BeTrue()) + *data = moduleStateData + return nil + }), + }) + }) + + It("should create generic vmclass when it doesn't exist", func() { + snapshots.GetMock.When(vmClassSnapshot).Then([]pkg.Snapshot{}) + + patchCollector.CreateMock.Set(func(obj interface{}) { + vmClass, ok := obj.(*v1alpha2.VirtualMachineClass) + Expect(ok).To(BeTrue()) + Expect(vmClass.Name).To(Equal("generic")) + }) + + Expect(Reconcile(context.Background(), newInput())).To(Succeed()) + Expect(patchCollector.CreateMock.Calls()).To(HaveLen(1)) + }) + + It("should not create generic vmclass when it already exists", func() { + snapshots.GetMock.When(vmClassSnapshot).Then([]pkg.Snapshot{ + mock.NewSnapshotMock(GinkgoT()), + }) + + patchCollector.CreateMock.Optional() + + Expect(Reconcile(context.Background(), newInput())).To(Succeed()) + Expect(patchCollector.CreateMock.Calls()).To(HaveLen(0)) + }) + }) + + Context("when module-state secret doesn't exist", func() { + BeforeEach(func() { + snapshots.GetMock.When(moduleStateSecretSnapshot).Then([]pkg.Snapshot{}) + }) + + It("should not create generic vmclass", func() { + snapshots.GetMock.When(vmClassSnapshot).Then([]pkg.Snapshot{}) + + patchCollector.CreateMock.Optional() + + Expect(Reconcile(context.Background(), newInput())).To(Succeed()) + Expect(patchCollector.CreateMock.Calls()).To(HaveLen(0)) + }) + }) + + Context("when module-state secret exists but doesn't contain generic-vmclass-created", func() { + BeforeEach(func() { + moduleStateData := map[string]interface{}{ + "other-key": base64.StdEncoding.EncodeToString([]byte("other-value")), + } + + snapshots.GetMock.When(moduleStateSecretSnapshot).Then([]pkg.Snapshot{ + mock.NewSnapshotMock(GinkgoT()).UnmarshalToMock.Set(func(v any) error { + data, ok := v.(*map[string]interface{}) + Expect(ok).To(BeTrue()) + *data = moduleStateData + return nil + }), + }) + }) + + It("should not create generic vmclass", func() { + snapshots.GetMock.When(vmClassSnapshot).Then([]pkg.Snapshot{}) + + patchCollector.CreateMock.Optional() + + Expect(Reconcile(context.Background(), newInput())).To(Succeed()) + Expect(patchCollector.CreateMock.Calls()).To(HaveLen(0)) + }) + }) +}) diff --git a/images/hooks/pkg/hooks/update-module-state/hook.go b/images/hooks/pkg/hooks/update-module-state/hook.go new file mode 100644 index 0000000000..980875733d --- /dev/null +++ b/images/hooks/pkg/hooks/update-module-state/hook.go @@ -0,0 +1,159 @@ +/* +Copyright 2025 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 update_module_state + +import ( + "context" + "encoding/base64" + "fmt" + + "hooks/pkg/settings" + + "github.com/deckhouse/virtualization/api/core" + "github.com/deckhouse/virtualization/api/core/v1alpha2" + + "github.com/deckhouse/module-sdk/pkg" + "github.com/deckhouse/module-sdk/pkg/registry" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" +) + +const ( + updateModuleStateHookName = "Update module-state secret" + vmClassSnapshot = "vmclass-generic" + moduleStateSecretSnapshot = "module-state-secret" + + genericVMClassName = "generic" + moduleStateSecretName = "module-state" + + apiVersion = core.GroupName + "/" + v1alpha2.Version +) + +var _ = registry.RegisterFunc(config, Reconcile) + +var config = &pkg.HookConfig{ + OnBeforeHelm: &pkg.OrderedConfig{Order: 15}, + Kubernetes: []pkg.KubernetesConfig{ + { + Name: vmClassSnapshot, + APIVersion: apiVersion, + Kind: v1alpha2.VirtualMachineClassKind, + JqFilter: `.metadata.name`, + NameSelector: &pkg.NameSelector{ + MatchNames: []string{genericVMClassName}, + }, + ExecuteHookOnSynchronization: ptr.To(false), + }, + { + Name: moduleStateSecretSnapshot, + APIVersion: "v1", + Kind: "Secret", + JqFilter: `.data`, + NameSelector: &pkg.NameSelector{ + MatchNames: []string{moduleStateSecretName}, + }, + NamespaceSelector: &pkg.NamespaceSelector{ + NameSelector: &pkg.NameSelector{ + MatchNames: []string{settings.ModuleNamespace}, + }, + }, + ExecuteHookOnSynchronization: ptr.To(false), + }, + }, + + Queue: fmt.Sprintf("modules/%s", settings.ModuleName), +} + +func Reconcile(_ context.Context, input *pkg.HookInput) error { + vmClasses := input.Snapshots.Get(vmClassSnapshot) + moduleStateSecrets := input.Snapshots.Get(moduleStateSecretSnapshot) + + vmClassExists := len(vmClasses) > 0 + + needsUpdate := false + currentState := false + + if len(moduleStateSecrets) > 0 { + moduleStateData := make(map[string]interface{}) + if err := moduleStateSecrets[0].UnmarshalTo(&moduleStateData); err == nil { + if genericCreatedEncoded, exists := moduleStateData["generic-vmclass-created"]; exists { + if encodedStr, ok := genericCreatedEncoded.(string); ok { + if decodedBytes, err := base64.StdEncoding.DecodeString(encodedStr); err == nil { + currentState = string(decodedBytes) == "true" + } + } + } + } + } + + if vmClassExists && !currentState { + needsUpdate = true + input.Logger.Info("Generic VirtualMachineClass exists but module-state doesn't reflect this, updating secret") + } else if !vmClassExists && currentState { + needsUpdate = true + input.Logger.Info("Generic VirtualMachineClass doesn't exist but module-state indicates it was created, updating secret") + } else if len(moduleStateSecrets) == 0 { + needsUpdate = true + input.Logger.Info("Module-state secret doesn't exist, creating it") + } else if vmClassExists && currentState { + input.Logger.Info("Module-state correctly reflects that generic vmclass exists") + } else { + input.Logger.Info("Module-state correctly reflects that generic vmclass doesn't exist") + } + + if needsUpdate { + if len(moduleStateSecrets) > 0 { + patchData := map[string]interface{}{ + "data": map[string]string{ + "generic-vmclass-created": base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%t", vmClassExists))), + }, + } + input.PatchCollector.PatchWithMerge(patchData, "v1", "Secret", settings.ModuleNamespace, moduleStateSecretName) + input.Logger.Info("Updated module-state secret") + } else { + secretData := map[string]string{ + "generic-vmclass-created": fmt.Sprintf("%t", vmClassExists), + } + + encodedData := make(map[string][]byte) + for key, value := range secretData { + encodedData[key] = []byte(base64.StdEncoding.EncodeToString([]byte(value))) + } + + secret := &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: moduleStateSecretName, + Namespace: settings.ModuleNamespace, + Labels: map[string]string{ + "module": settings.ModuleName, + }, + }, + Data: encodedData, + Type: "Opaque", + } + input.PatchCollector.Create(secret) + input.Logger.Info("Created module-state secret") + } + } + + return nil +} diff --git a/images/hooks/pkg/hooks/update-module-state/hook_test.go b/images/hooks/pkg/hooks/update-module-state/hook_test.go new file mode 100644 index 0000000000..cdcadcb151 --- /dev/null +++ b/images/hooks/pkg/hooks/update-module-state/hook_test.go @@ -0,0 +1,213 @@ +/* +Copyright 2025 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 update_module_state + +import ( + "context" + "encoding/base64" + "testing" + + "hooks/pkg/settings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/deckhouse/deckhouse/pkg/log" + "github.com/deckhouse/module-sdk/pkg" + "github.com/deckhouse/module-sdk/testing/mock" + corev1 "k8s.io/api/core/v1" +) + +func TestUpdateModuleState(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Update Module State Suite") +} + +var _ = Describe("Update Module State hook", func() { + var ( + snapshots *mock.SnapshotsMock + patchCollector *mock.PatchCollectorMock + ) + + newInput := func() *pkg.HookInput { + return &pkg.HookInput{ + Snapshots: snapshots, + PatchCollector: patchCollector, + Logger: log.NewNop(), + } + } + + BeforeEach(func() { + snapshots = mock.NewSnapshotsMock(GinkgoT()) + patchCollector = mock.NewPatchCollectorMock(GinkgoT()) + }) + + AfterEach(func() { + snapshots = nil + patchCollector = nil + }) + + Context("when generic vmclass exists", func() { + BeforeEach(func() { + snapshots.GetMock.When(vmClassSnapshot).Then([]pkg.Snapshot{ + mock.NewSnapshotMock(GinkgoT()), + }) + }) + + It("should create module-state secret when it doesn't exist", func() { + snapshots.GetMock.When(moduleStateSecretSnapshot).Then([]pkg.Snapshot{}) + + patchCollector.CreateMock.Set(func(obj interface{}) { + secret, ok := obj.(*corev1.Secret) + Expect(ok).To(BeTrue()) + Expect(secret.Name).To(Equal("module-state")) + Expect(secret.Namespace).To(Equal(settings.ModuleNamespace)) + Expect(secret.Data).To(HaveKey("generic-vmclass-created")) + + decoded, err := base64.StdEncoding.DecodeString(string(secret.Data["generic-vmclass-created"])) + Expect(err).ToNot(HaveOccurred()) + Expect(string(decoded)).To(Equal("true")) + }) + + patchCollector.PatchWithMergeMock.Optional() + + Expect(Reconcile(context.Background(), newInput())).To(Succeed()) + Expect(patchCollector.CreateMock.Calls()).To(HaveLen(1)) + Expect(patchCollector.PatchWithMergeMock.Calls()).To(HaveLen(0)) + }) + + It("should update module-state secret when it exists but has wrong value", func() { + moduleStateData := map[string]interface{}{ + "generic-vmclass-created": base64.StdEncoding.EncodeToString([]byte("false")), + } + + snapshots.GetMock.When(moduleStateSecretSnapshot).Then([]pkg.Snapshot{ + mock.NewSnapshotMock(GinkgoT()).UnmarshalToMock.Set(func(v any) error { + data, ok := v.(*map[string]interface{}) + Expect(ok).To(BeTrue()) + *data = moduleStateData + return nil + }), + }) + + patchCollector.PatchWithMergeMock.Set(func(obj interface{}, apiVersion, kind, namespace, name string, opts ...pkg.PatchCollectorOption) { + patchData, ok := obj.(map[string]interface{}) + Expect(ok).To(BeTrue()) + Expect(patchData).To(HaveKey("data")) + + data, ok := patchData["data"].(map[string]string) + Expect(ok).To(BeTrue()) + Expect(data).To(HaveKey("generic-vmclass-created")) + + decoded, err := base64.StdEncoding.DecodeString(data["generic-vmclass-created"]) + Expect(err).ToNot(HaveOccurred()) + Expect(string(decoded)).To(Equal("true")) + }) + + patchCollector.CreateMock.Optional() + + Expect(Reconcile(context.Background(), newInput())).To(Succeed()) + Expect(patchCollector.PatchWithMergeMock.Calls()).To(HaveLen(1)) + Expect(patchCollector.CreateMock.Calls()).To(HaveLen(0)) + }) + + It("should not update module-state secret when it has correct value", func() { + moduleStateData := map[string]interface{}{ + "generic-vmclass-created": base64.StdEncoding.EncodeToString([]byte("true")), + } + + snapshots.GetMock.When(moduleStateSecretSnapshot).Then([]pkg.Snapshot{ + mock.NewSnapshotMock(GinkgoT()).UnmarshalToMock.Set(func(v any) error { + data, ok := v.(*map[string]interface{}) + Expect(ok).To(BeTrue()) + *data = moduleStateData + return nil + }), + }) + + patchCollector.CreateMock.Optional() + patchCollector.PatchWithMergeMock.Optional() + + Expect(Reconcile(context.Background(), newInput())).To(Succeed()) + Expect(patchCollector.CreateMock.Calls()).To(HaveLen(0)) + Expect(patchCollector.PatchWithMergeMock.Calls()).To(HaveLen(0)) + }) + }) + + Context("when generic vmclass doesn't exist", func() { + BeforeEach(func() { + snapshots.GetMock.When(vmClassSnapshot).Then([]pkg.Snapshot{}) + }) + + It("should create module-state secret with false value when it doesn't exist", func() { + snapshots.GetMock.When(moduleStateSecretSnapshot).Then([]pkg.Snapshot{}) + + patchCollector.CreateMock.Set(func(obj interface{}) { + secret, ok := obj.(*corev1.Secret) + Expect(ok).To(BeTrue()) + Expect(secret.Name).To(Equal("module-state")) + Expect(secret.Namespace).To(Equal(settings.ModuleNamespace)) + Expect(secret.Data).To(HaveKey("generic-vmclass-created")) + + decoded, err := base64.StdEncoding.DecodeString(string(secret.Data["generic-vmclass-created"])) + Expect(err).ToNot(HaveOccurred()) + Expect(string(decoded)).To(Equal("false")) + }) + + patchCollector.PatchWithMergeMock.Optional() + + Expect(Reconcile(context.Background(), newInput())).To(Succeed()) + Expect(patchCollector.CreateMock.Calls()).To(HaveLen(1)) + Expect(patchCollector.PatchWithMergeMock.Calls()).To(HaveLen(0)) + }) + + It("should update module-state secret when it exists but has wrong value", func() { + moduleStateData := map[string]interface{}{ + "generic-vmclass-created": base64.StdEncoding.EncodeToString([]byte("true")), + } + + snapshots.GetMock.When(moduleStateSecretSnapshot).Then([]pkg.Snapshot{ + mock.NewSnapshotMock(GinkgoT()).UnmarshalToMock.Set(func(v any) error { + data, ok := v.(*map[string]interface{}) + Expect(ok).To(BeTrue()) + *data = moduleStateData + return nil + }), + }) + + patchCollector.PatchWithMergeMock.Set(func(obj interface{}, apiVersion, kind, namespace, name string, opts ...pkg.PatchCollectorOption) { + patchData, ok := obj.(map[string]interface{}) + Expect(ok).To(BeTrue()) + Expect(patchData).To(HaveKey("data")) + + data, ok := patchData["data"].(map[string]string) + Expect(ok).To(BeTrue()) + Expect(data).To(HaveKey("generic-vmclass-created")) + + decoded, err := base64.StdEncoding.DecodeString(data["generic-vmclass-created"]) + Expect(err).ToNot(HaveOccurred()) + Expect(string(decoded)).To(Equal("false")) + }) + + patchCollector.CreateMock.Optional() + + Expect(Reconcile(context.Background(), newInput())).To(Succeed()) + Expect(patchCollector.PatchWithMergeMock.Calls()).To(HaveLen(1)) + Expect(patchCollector.CreateMock.Calls()).To(HaveLen(0)) + }) + }) +}) diff --git a/templates/virtualization-controller/vmclasses-default.yaml b/templates/virtualization-controller/vmclasses-default.yaml deleted file mode 100644 index f2beb27764..0000000000 --- a/templates/virtualization-controller/vmclasses-default.yaml +++ /dev/null @@ -1,34 +0,0 @@ -apiVersion: virtualization.deckhouse.io/v1alpha2 -kind: VirtualMachineClass -metadata: - name: generic - {{- include "helm_lib_module_labels" (list . (dict "app" "virtualization-controller")) | nindent 2 }} -spec: - nodeSelector: - matchExpressions: - - key: node-role.kubernetes.io/control-plane - operator: DoesNotExist - cpu: - type: Model - model: Nehalem - sizingPolicies: - - cores: - min: 1 - max: 4 - dedicatedCores: [false] - coreFractions: [5, 10, 20, 50, 100] - - cores: - min: 5 - max: 8 - dedicatedCores: [false] - coreFractions: [20, 50, 100] - - cores: - min: 9 - max: 16 - dedicatedCores: [true, false] - coreFractions: [50, 100] - - cores: - min: 17 - max: 1024 - dedicatedCores: [true, false] - coreFractions: [100] From d0744f1ca87d4fc5595a326ae4225e7de3bcdde2 Mon Sep 17 00:00:00 2001 From: Pavel Tishkov Date: Wed, 15 Oct 2025 13:56:52 +0300 Subject: [PATCH 02/17] fix(module): upd Signed-off-by: Pavel Tishkov --- .../pkg/hooks/create-generic-vmclass/hook.go | 10 ----- .../pkg/hooks/update-module-state/hook.go | 45 ++++++++++++------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/images/hooks/pkg/hooks/create-generic-vmclass/hook.go b/images/hooks/pkg/hooks/create-generic-vmclass/hook.go index 70d1be162d..f35b728645 100644 --- a/images/hooks/pkg/hooks/create-generic-vmclass/hook.go +++ b/images/hooks/pkg/hooks/create-generic-vmclass/hook.go @@ -79,18 +79,15 @@ var config = &pkg.HookConfig{ } func Reconcile(_ context.Context, input *pkg.HookInput) error { - // Проверяем, есть ли запись о том, что generic vmclass был создан ранее moduleStateSecrets := input.Snapshots.Get(moduleStateSecretSnapshot) vmClasses := input.Snapshots.Get(vmClassSnapshot) - // Если секрет module-state существует и содержит информацию о создании generic vmclass shouldCreateVMClass := false if len(moduleStateSecrets) > 0 { moduleStateData := make(map[string]interface{}) if err := moduleStateSecrets[0].UnmarshalTo(&moduleStateData); err == nil { if genericCreatedEncoded, exists := moduleStateData["generic-vmclass-created"]; exists { if encodedStr, ok := genericCreatedEncoded.(string); ok { - // Декодируем base64 строку if decodedBytes, err := base64.StdEncoding.DecodeString(encodedStr); err == nil { if string(decodedBytes) == "true" { shouldCreateVMClass = true @@ -102,12 +99,8 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { } } - // Проверяем, существует ли generic vmclass vmClassExists := len(vmClasses) > 0 - // Создаем vmclass generic если: - // 1. В секрете module-state есть запись о том, что он был создан ранее - // 2. И vmclass generic отсутствует if shouldCreateVMClass && !vmClassExists { input.Logger.Info("Creating generic VirtualMachineClass as it was previously created but is now missing") @@ -121,9 +114,6 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { Labels: map[string]string{ "module": settings.ModuleName, }, - Annotations: map[string]string{ - "helm.sh/resource-policy": "keep", - }, }, Spec: v1alpha2.VirtualMachineClassSpec{ CPU: v1alpha2.CPU{ diff --git a/images/hooks/pkg/hooks/update-module-state/hook.go b/images/hooks/pkg/hooks/update-module-state/hook.go index 980875733d..66327b1e3c 100644 --- a/images/hooks/pkg/hooks/update-module-state/hook.go +++ b/images/hooks/pkg/hooks/update-module-state/hook.go @@ -42,6 +42,9 @@ const ( moduleStateSecretName = "module-state" apiVersion = core.GroupName + "/" + v1alpha2.Version + + // State fields configuration + genericVMClassStateKey = "generic-vmclass-created" ) var _ = registry.RegisterFunc(config, Reconcile) @@ -79,6 +82,26 @@ var config = &pkg.HookConfig{ Queue: fmt.Sprintf("modules/%s", settings.ModuleName), } +type ModuleState struct { + GenericVMClassCreated bool +} + +func (ms ModuleState) ToSecretData() map[string][]byte { + value := fmt.Sprintf("%t", ms.GenericVMClassCreated) + return map[string][]byte{ + genericVMClassStateKey: []byte(base64.StdEncoding.EncodeToString([]byte(value))), + } +} + +func (ms ModuleState) ToPatchData() map[string]interface{} { + value := fmt.Sprintf("%t", ms.GenericVMClassCreated) + return map[string]interface{}{ + "data": map[string]string{ + genericVMClassStateKey: base64.StdEncoding.EncodeToString([]byte(value)), + }, + } +} + func Reconcile(_ context.Context, input *pkg.HookInput) error { vmClasses := input.Snapshots.Get(vmClassSnapshot) moduleStateSecrets := input.Snapshots.Get(moduleStateSecretSnapshot) @@ -91,7 +114,7 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { if len(moduleStateSecrets) > 0 { moduleStateData := make(map[string]interface{}) if err := moduleStateSecrets[0].UnmarshalTo(&moduleStateData); err == nil { - if genericCreatedEncoded, exists := moduleStateData["generic-vmclass-created"]; exists { + if genericCreatedEncoded, exists := moduleStateData[genericVMClassStateKey]; exists { if encodedStr, ok := genericCreatedEncoded.(string); ok { if decodedBytes, err := base64.StdEncoding.DecodeString(encodedStr); err == nil { currentState = string(decodedBytes) == "true" @@ -117,24 +140,12 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { } if needsUpdate { + state := ModuleState{GenericVMClassCreated: vmClassExists} + if len(moduleStateSecrets) > 0 { - patchData := map[string]interface{}{ - "data": map[string]string{ - "generic-vmclass-created": base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%t", vmClassExists))), - }, - } - input.PatchCollector.PatchWithMerge(patchData, "v1", "Secret", settings.ModuleNamespace, moduleStateSecretName) + input.PatchCollector.PatchWithMerge(state.ToPatchData(), "v1", "Secret", settings.ModuleNamespace, moduleStateSecretName) input.Logger.Info("Updated module-state secret") } else { - secretData := map[string]string{ - "generic-vmclass-created": fmt.Sprintf("%t", vmClassExists), - } - - encodedData := make(map[string][]byte) - for key, value := range secretData { - encodedData[key] = []byte(base64.StdEncoding.EncodeToString([]byte(value))) - } - secret := &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: "v1", @@ -147,7 +158,7 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { "module": settings.ModuleName, }, }, - Data: encodedData, + Data: state.ToSecretData(), Type: "Opaque", } input.PatchCollector.Create(secret) From d5a73e29053936c0a065e6b030662bf8ff1d5c1c Mon Sep 17 00:00:00 2001 From: Pavel Tishkov Date: Mon, 20 Oct 2025 08:33:38 +0300 Subject: [PATCH 03/17] add vmclass labels Signed-off-by: Pavel Tishkov --- .../hooks/pkg/hooks/create-generic-vmclass/hook.go | 12 +++++++++--- .../pkg/hooks/create-generic-vmclass/hook_test.go | 4 ++++ images/hooks/pkg/hooks/update-module-state/hook.go | 6 ++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/images/hooks/pkg/hooks/create-generic-vmclass/hook.go b/images/hooks/pkg/hooks/create-generic-vmclass/hook.go index f35b728645..7aca869e49 100644 --- a/images/hooks/pkg/hooks/create-generic-vmclass/hook.go +++ b/images/hooks/pkg/hooks/create-generic-vmclass/hook.go @@ -33,9 +33,8 @@ import ( ) const ( - createGenericVMClassHookName = "Create generic VirtualMachineClass" - moduleStateSecretSnapshot = "module-state-secret" - vmClassSnapshot = "vmclass-generic" + moduleStateSecretSnapshot = "module-state-secret" + vmClassSnapshot = "vmclass-generic" moduleStateSecretName = "module-state" genericVMClassName = "generic" @@ -71,6 +70,12 @@ var config = &pkg.HookConfig{ NameSelector: &pkg.NameSelector{ MatchNames: []string{genericVMClassName}, }, + LabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "virtualization-controller", + "module": settings.ModuleName, + }, + }, ExecuteHookOnSynchronization: ptr.To(false), }, }, @@ -112,6 +117,7 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { ObjectMeta: metav1.ObjectMeta{ Name: genericVMClassName, Labels: map[string]string{ + "app": "virtualization-controller", "module": settings.ModuleName, }, }, diff --git a/images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go b/images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go index d4dbd78feb..acbcf11f9c 100644 --- a/images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go +++ b/images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go @@ -82,6 +82,10 @@ var _ = Describe("Create Generic VMClass hook", func() { vmClass, ok := obj.(*v1alpha2.VirtualMachineClass) Expect(ok).To(BeTrue()) Expect(vmClass.Name).To(Equal("generic")) + Expect(vmClass.Labels).To(Equal(map[string]string{ + "app": "virtualization-controller", + "module": "virtualization", + })) }) Expect(Reconcile(context.Background(), newInput())).To(Succeed()) diff --git a/images/hooks/pkg/hooks/update-module-state/hook.go b/images/hooks/pkg/hooks/update-module-state/hook.go index 66327b1e3c..dfde251ab9 100644 --- a/images/hooks/pkg/hooks/update-module-state/hook.go +++ b/images/hooks/pkg/hooks/update-module-state/hook.go @@ -60,6 +60,12 @@ var config = &pkg.HookConfig{ NameSelector: &pkg.NameSelector{ MatchNames: []string{genericVMClassName}, }, + LabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "virtualization-controller", + "module": settings.ModuleName, + }, + }, ExecuteHookOnSynchronization: ptr.To(false), }, { From 234e961c917b71aa8124a3afec785d4580cdd14d Mon Sep 17 00:00:00 2001 From: Pavel Tishkov Date: Mon, 20 Oct 2025 09:58:11 +0300 Subject: [PATCH 04/17] fix logic Signed-off-by: Pavel Tishkov --- .../pkg/hooks/create-generic-vmclass/hook.go | 6 +++--- .../pkg/hooks/update-module-state/hook.go | 7 +++---- .../hooks/update-module-state/hook_test.go | 19 +++---------------- 3 files changed, 9 insertions(+), 23 deletions(-) diff --git a/images/hooks/pkg/hooks/create-generic-vmclass/hook.go b/images/hooks/pkg/hooks/create-generic-vmclass/hook.go index 7aca869e49..7511be2a1a 100644 --- a/images/hooks/pkg/hooks/create-generic-vmclass/hook.go +++ b/images/hooks/pkg/hooks/create-generic-vmclass/hook.go @@ -96,7 +96,7 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { if decodedBytes, err := base64.StdEncoding.DecodeString(encodedStr); err == nil { if string(decodedBytes) == "true" { shouldCreateVMClass = true - input.Logger.Info("Found record in module-state that generic vmclass was created previously") + input.Logger.Info("Found record in module-state that generic VirtualMachineClass was created previously") } } } @@ -107,7 +107,7 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { vmClassExists := len(vmClasses) > 0 if shouldCreateVMClass && !vmClassExists { - input.Logger.Info("Creating generic VirtualMachineClass as it was previously created but is now missing") + input.Logger.Info("Creating generic VirtualMachineClass") vmClass := &v1alpha2.VirtualMachineClass{ TypeMeta: metav1.TypeMeta{ @@ -168,7 +168,7 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { } else if shouldCreateVMClass && vmClassExists { input.Logger.Info("Generic VirtualMachineClass already exists, no action needed") } else if !shouldCreateVMClass && !vmClassExists { - input.Logger.Info("No record of generic vmclass creation in module-state, skipping creation") + input.Logger.Info("No record of generic VirtualMachineClass creation in module-state, skipping creation") } return nil diff --git a/images/hooks/pkg/hooks/update-module-state/hook.go b/images/hooks/pkg/hooks/update-module-state/hook.go index dfde251ab9..5cac8e64c0 100644 --- a/images/hooks/pkg/hooks/update-module-state/hook.go +++ b/images/hooks/pkg/hooks/update-module-state/hook.go @@ -134,15 +134,14 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { needsUpdate = true input.Logger.Info("Generic VirtualMachineClass exists but module-state doesn't reflect this, updating secret") } else if !vmClassExists && currentState { - needsUpdate = true - input.Logger.Info("Generic VirtualMachineClass doesn't exist but module-state indicates it was created, updating secret") + input.Logger.Info("Generic VirtualMachineClass doesn't exist but module-state indicates it was created previously - keeping historical record") } else if len(moduleStateSecrets) == 0 { needsUpdate = true input.Logger.Info("Module-state secret doesn't exist, creating it") } else if vmClassExists && currentState { - input.Logger.Info("Module-state correctly reflects that generic vmclass exists") + input.Logger.Info("Module-state correctly reflects that generic VirtualMachineClass exists") } else { - input.Logger.Info("Module-state correctly reflects that generic vmclass doesn't exist") + input.Logger.Info("Module-state correctly reflects that generic VirtualMachineClass doesn't exist") } if needsUpdate { diff --git a/images/hooks/pkg/hooks/update-module-state/hook_test.go b/images/hooks/pkg/hooks/update-module-state/hook_test.go index cdcadcb151..ae5f34f2e9 100644 --- a/images/hooks/pkg/hooks/update-module-state/hook_test.go +++ b/images/hooks/pkg/hooks/update-module-state/hook_test.go @@ -175,7 +175,7 @@ var _ = Describe("Update Module State hook", func() { Expect(patchCollector.PatchWithMergeMock.Calls()).To(HaveLen(0)) }) - It("should update module-state secret when it exists but has wrong value", func() { + It("should keep historical record when vmclass doesn't exist but module-state indicates it was created", func() { moduleStateData := map[string]interface{}{ "generic-vmclass-created": base64.StdEncoding.EncodeToString([]byte("true")), } @@ -189,24 +189,11 @@ var _ = Describe("Update Module State hook", func() { }), }) - patchCollector.PatchWithMergeMock.Set(func(obj interface{}, apiVersion, kind, namespace, name string, opts ...pkg.PatchCollectorOption) { - patchData, ok := obj.(map[string]interface{}) - Expect(ok).To(BeTrue()) - Expect(patchData).To(HaveKey("data")) - - data, ok := patchData["data"].(map[string]string) - Expect(ok).To(BeTrue()) - Expect(data).To(HaveKey("generic-vmclass-created")) - - decoded, err := base64.StdEncoding.DecodeString(data["generic-vmclass-created"]) - Expect(err).ToNot(HaveOccurred()) - Expect(string(decoded)).To(Equal("false")) - }) - + patchCollector.PatchWithMergeMock.Optional() patchCollector.CreateMock.Optional() Expect(Reconcile(context.Background(), newInput())).To(Succeed()) - Expect(patchCollector.PatchWithMergeMock.Calls()).To(HaveLen(1)) + Expect(patchCollector.PatchWithMergeMock.Calls()).To(HaveLen(0)) Expect(patchCollector.CreateMock.Calls()).To(HaveLen(0)) }) }) From 16e7fe559761a6eb862b3517e5aa1256a5366716 Mon Sep 17 00:00:00 2001 From: Pavel Tishkov Date: Tue, 21 Oct 2025 08:11:07 +0300 Subject: [PATCH 05/17] add hook to drop labels Signed-off-by: Pavel Tishkov --- .../virtualization-module-hooks/register.go | 1 + .../hook.go | 141 ++++++++++++++++ .../hook_test.go | 153 ++++++++++++++++++ 3 files changed, 295 insertions(+) create mode 100644 images/hooks/pkg/hooks/drop-helm-labels-from-generic-vmclass/hook.go create mode 100644 images/hooks/pkg/hooks/drop-helm-labels-from-generic-vmclass/hook_test.go diff --git a/images/hooks/cmd/virtualization-module-hooks/register.go b/images/hooks/cmd/virtualization-module-hooks/register.go index e906c1b981..49be3456f6 100644 --- a/images/hooks/cmd/virtualization-module-hooks/register.go +++ b/images/hooks/cmd/virtualization-module-hooks/register.go @@ -21,6 +21,7 @@ import ( _ "hooks/pkg/hooks/create-generic-vmclass" _ "hooks/pkg/hooks/discovery-clusterip-service-for-dvcr" _ "hooks/pkg/hooks/discovery-workload-nodes" + _ "hooks/pkg/hooks/drop-helm-labels-from-generic-vmclass" _ "hooks/pkg/hooks/drop-openshift-labels" _ "hooks/pkg/hooks/generate-secret-for-dvcr" _ "hooks/pkg/hooks/migrate-delete-renamed-validation-admission-policy" diff --git a/images/hooks/pkg/hooks/drop-helm-labels-from-generic-vmclass/hook.go b/images/hooks/pkg/hooks/drop-helm-labels-from-generic-vmclass/hook.go new file mode 100644 index 0000000000..4e46e769f1 --- /dev/null +++ b/images/hooks/pkg/hooks/drop-helm-labels-from-generic-vmclass/hook.go @@ -0,0 +1,141 @@ +/* +Copyright 2025 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 drop_helm_labels_from_generic_vmclass + +import ( + "context" + "fmt" + "strings" + + "github.com/deckhouse/module-sdk/pkg" + "github.com/deckhouse/module-sdk/pkg/registry" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + "hooks/pkg/settings" +) + +const ( + vmClassSnapshot = "vmclass-generic" + genericVMClassName = "generic" +) + +const ( + helmManagedByLabel = "app.kubernetes.io/managed-by" + helmHeritageLabel = "heritage" +) + +var _ = registry.RegisterFunc(configDropHelmLabels, handlerDropHelmLabels) + +var configDropHelmLabels = &pkg.HookConfig{ + OnAfterHelm: &pkg.OrderedConfig{Order: 10}, + Kubernetes: []pkg.KubernetesConfig{ + { + Name: vmClassSnapshot, + APIVersion: "deckhouse.io/v1alpha2", + Kind: "VirtualMachineClass", + JqFilter: ".metadata", + NameSelector: &pkg.NameSelector{ + MatchNames: []string{genericVMClassName}, + }, + LabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "virtualization-controller", + "app.kubernetes.io/managed-by": "Helm", + "heritage": "deckhouse", + "module": settings.ModuleName, + }, + }, + ExecuteHookOnEvents: ptr.To(false), + }, + }, + + Queue: fmt.Sprintf("modules/%s", settings.ModuleName), +} + +type VMClassMetadata struct { + Name string `json:"name"` + Labels map[string]string `json:"labels"` +} + +func handlerDropHelmLabels(_ context.Context, input *pkg.HookInput) error { + snaps := input.Snapshots.Get(vmClassSnapshot) + if len(snaps) == 0 { + return nil + } + + vmClass := &VMClassMetadata{} + err := snaps[0].UnmarshalTo(vmClass) + if err != nil { + input.Logger.Error("failed to unmarshal VMClass", "error", err) + return err + } + + if vmClass.Labels == nil { + return nil + } + + // Check if VMClass has all required labels to be processed + if vmClass.Labels["app"] != "virtualization-controller" || + vmClass.Labels["module"] != settings.ModuleName || + vmClass.Labels[helmManagedByLabel] != "Helm" || + vmClass.Labels[helmHeritageLabel] != "deckhouse" { + input.Logger.Debug("VMClass doesn't match required labels, skipping") + return nil + } + + var patches []map[string]interface{} + hasLabelsToRemove := false + + // Check and prepare patches for Helm labels + if _, exists := vmClass.Labels[helmManagedByLabel]; exists { + patches = append(patches, map[string]interface{}{ + "op": "remove", + "path": fmt.Sprintf("/metadata/labels/%s", jsonPatchEscape(helmManagedByLabel)), + "value": nil, + }) + hasLabelsToRemove = true + } + + if _, exists := vmClass.Labels[helmHeritageLabel]; exists { + patches = append(patches, map[string]interface{}{ + "op": "remove", + "path": fmt.Sprintf("/metadata/labels/%s", jsonPatchEscape(helmHeritageLabel)), + "value": nil, + }) + hasLabelsToRemove = true + } + + if !hasLabelsToRemove { + return nil + } + + input.Logger.Info("Removing Helm labels from generic VMClass") + input.PatchCollector.PatchWithJSON( + patches, + "deckhouse.io/v1alpha2", + "VirtualMachineClass", + "", + genericVMClassName, + ) + + return nil +} + +func jsonPatchEscape(s string) string { + return strings.NewReplacer("~", "~0", "/", "~1").Replace(s) +} diff --git a/images/hooks/pkg/hooks/drop-helm-labels-from-generic-vmclass/hook_test.go b/images/hooks/pkg/hooks/drop-helm-labels-from-generic-vmclass/hook_test.go new file mode 100644 index 0000000000..4871c7247c --- /dev/null +++ b/images/hooks/pkg/hooks/drop-helm-labels-from-generic-vmclass/hook_test.go @@ -0,0 +1,153 @@ +/* +Copyright 2025 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 drop_helm_labels_from_generic_vmclass + +import ( + "context" + "fmt" + "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" +) + +func TestDropHelmLabelsFromGenericVMClass(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Drop Helm labels from generic VMClass Suite") +} + +var _ = Describe("Drop Helm labels from generic VMClass", func() { + var ( + snapshots *mock.SnapshotsMock + patchCollector *mock.PatchCollectorMock + ) + + newInput := func() *pkg.HookInput { + return &pkg.HookInput{ + Snapshots: snapshots, + PatchCollector: patchCollector, + Logger: log.NewNop(), + } + } + + newSnapshot := func(withManagedBy, withHeritage bool) pkg.Snapshot { + return mock.NewSnapshotMock(GinkgoT()).UnmarshalToMock.Set(func(v any) (err error) { + obj, ok := v.(*VMClassMetadata) + Expect(ok).To(BeTrue()) + obj.Name = genericVMClassName + obj.Labels = make(map[string]string) + + // Required labels for VMClass to be found by the hook + obj.Labels["app"] = "virtualization-controller" + obj.Labels["module"] = "virtualization" + + if withManagedBy { + obj.Labels[helmManagedByLabel] = "Helm" + } + if withHeritage { + obj.Labels[helmHeritageLabel] = "deckhouse" + } + + return nil + }) + } + + setSnapshots := func(snaps ...pkg.Snapshot) { + snapshots.GetMock.When(vmClassSnapshot).Then(snaps) + } + + BeforeEach(func() { + snapshots = mock.NewSnapshotsMock(GinkgoT()) + patchCollector = mock.NewPatchCollectorMock(GinkgoT()) + }) + + It("Should drop both Helm labels from generic VMClass with all required labels", func() { + setSnapshots(newSnapshot(true, true)) + patchCollector.PatchWithJSONMock.Set(func(patch any, apiVersion, kind, namespace, name string, opts ...pkg.PatchCollectorOption) { + Expect(apiVersion).To(Equal("deckhouse.io/v1alpha2")) + Expect(kind).To(Equal("VirtualMachineClass")) + Expect(namespace).To(Equal("")) + Expect(name).To(Equal(genericVMClassName)) + Expect(opts).To(HaveLen(0)) + + jsonPatch, ok := patch.([]map[string]interface{}) + Expect(ok).To(BeTrue()) + Expect(jsonPatch).To(HaveLen(2)) + + // Check first patch (managed-by label) + Expect(jsonPatch[0]["op"]).To(Equal("remove")) + Expect(jsonPatch[0]["path"]).To(Equal(fmt.Sprintf("/metadata/labels/%s", jsonPatchEscape(helmManagedByLabel)))) + Expect(jsonPatch[0]["value"]).To(BeNil()) + + // Check second patch (heritage label) + Expect(jsonPatch[1]["op"]).To(Equal("remove")) + Expect(jsonPatch[1]["path"]).To(Equal(fmt.Sprintf("/metadata/labels/%s", jsonPatchEscape(helmHeritageLabel)))) + Expect(jsonPatch[1]["value"]).To(BeNil()) + }) + + Expect(handlerDropHelmLabels(context.Background(), newInput())).To(Succeed()) + }) + + It("Should do nothing when VMClass doesn't have all required labels", func() { + // Create a snapshot with VMClass that has only some required labels + partialLabelSnapshot := mock.NewSnapshotMock(GinkgoT()).UnmarshalToMock.Set(func(v any) (err error) { + obj, ok := v.(*VMClassMetadata) + Expect(ok).To(BeTrue()) + obj.Name = genericVMClassName + obj.Labels = make(map[string]string) + + // Only some required labels - VMClass won't be processed + obj.Labels["app"] = "virtualization-controller" + obj.Labels["module"] = "virtualization" + // Missing helmManagedByLabel and helmHeritageLabel + + return nil + }) + + setSnapshots(partialLabelSnapshot) + Expect(handlerDropHelmLabels(context.Background(), newInput())).To(Succeed()) + }) + + It("Should do nothing when VMClass not found", func() { + setSnapshots() + Expect(handlerDropHelmLabels(context.Background(), newInput())).To(Succeed()) + }) + + It("Should do nothing when VMClass exists but doesn't match label selector", func() { + // Create a snapshot with VMClass that has wrong labels + wrongLabelSnapshot := mock.NewSnapshotMock(GinkgoT()).UnmarshalToMock.Set(func(v any) (err error) { + obj, ok := v.(*VMClassMetadata) + Expect(ok).To(BeTrue()) + obj.Name = genericVMClassName + obj.Labels = make(map[string]string) + + // Wrong labels - VMClass won't be found by the hook + obj.Labels["app"] = "wrong-app" + obj.Labels["module"] = "wrong-module" + obj.Labels[helmManagedByLabel] = "Helm" + obj.Labels[helmHeritageLabel] = "deckhouse" + + return nil + }) + + setSnapshots(wrongLabelSnapshot) + Expect(handlerDropHelmLabels(context.Background(), newInput())).To(Succeed()) + }) +}) From b0475aef23fab9c8a3098fdd031d753df9bbc440 Mon Sep 17 00:00:00 2001 From: Pavel Tishkov Date: Tue, 21 Oct 2025 08:17:04 +0300 Subject: [PATCH 06/17] fix Signed-off-by: Pavel Tishkov --- images/hooks/pkg/hooks/create-generic-vmclass/hook.go | 6 ++++-- .../hooks/drop-helm-labels-from-generic-vmclass/hook.go | 9 +++++---- .../drop-helm-labels-from-generic-vmclass/hook_test.go | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/images/hooks/pkg/hooks/create-generic-vmclass/hook.go b/images/hooks/pkg/hooks/create-generic-vmclass/hook.go index 7511be2a1a..0c6d584803 100644 --- a/images/hooks/pkg/hooks/create-generic-vmclass/hook.go +++ b/images/hooks/pkg/hooks/create-generic-vmclass/hook.go @@ -123,8 +123,10 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { }, Spec: v1alpha2.VirtualMachineClassSpec{ CPU: v1alpha2.CPU{ - Type: v1alpha2.CPUTypeModel, - Model: "Nehalem", + Type: v1alpha2.CPUTypeModel, + Model: "Nehalem", + Features: nil, + Discovery: v1alpha2.CpuDiscovery{}, }, SizingPolicies: []v1alpha2.SizingPolicy{ { diff --git a/images/hooks/pkg/hooks/drop-helm-labels-from-generic-vmclass/hook.go b/images/hooks/pkg/hooks/drop-helm-labels-from-generic-vmclass/hook.go index 4e46e769f1..c9f6e4c571 100644 --- a/images/hooks/pkg/hooks/drop-helm-labels-from-generic-vmclass/hook.go +++ b/images/hooks/pkg/hooks/drop-helm-labels-from-generic-vmclass/hook.go @@ -23,6 +23,7 @@ import ( "github.com/deckhouse/module-sdk/pkg" "github.com/deckhouse/module-sdk/pkg/registry" + "github.com/deckhouse/virtualization/api/core/v1alpha2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" @@ -42,12 +43,12 @@ const ( var _ = registry.RegisterFunc(configDropHelmLabels, handlerDropHelmLabels) var configDropHelmLabels = &pkg.HookConfig{ - OnAfterHelm: &pkg.OrderedConfig{Order: 10}, + OnAfterHelm: &pkg.OrderedConfig{Order: 20}, Kubernetes: []pkg.KubernetesConfig{ { Name: vmClassSnapshot, - APIVersion: "deckhouse.io/v1alpha2", - Kind: "VirtualMachineClass", + APIVersion: "virtualization.deckhouse.io/v1alpha2", + Kind: v1alpha2.VirtualMachineClassKind, JqFilter: ".metadata", NameSelector: &pkg.NameSelector{ MatchNames: []string{genericVMClassName}, @@ -127,7 +128,7 @@ func handlerDropHelmLabels(_ context.Context, input *pkg.HookInput) error { input.Logger.Info("Removing Helm labels from generic VMClass") input.PatchCollector.PatchWithJSON( patches, - "deckhouse.io/v1alpha2", + "virtualization.deckhouse.io/v1alpha2", "VirtualMachineClass", "", genericVMClassName, diff --git a/images/hooks/pkg/hooks/drop-helm-labels-from-generic-vmclass/hook_test.go b/images/hooks/pkg/hooks/drop-helm-labels-from-generic-vmclass/hook_test.go index 4871c7247c..673e2a0db5 100644 --- a/images/hooks/pkg/hooks/drop-helm-labels-from-generic-vmclass/hook_test.go +++ b/images/hooks/pkg/hooks/drop-helm-labels-from-generic-vmclass/hook_test.go @@ -81,7 +81,7 @@ var _ = Describe("Drop Helm labels from generic VMClass", func() { It("Should drop both Helm labels from generic VMClass with all required labels", func() { setSnapshots(newSnapshot(true, true)) patchCollector.PatchWithJSONMock.Set(func(patch any, apiVersion, kind, namespace, name string, opts ...pkg.PatchCollectorOption) { - Expect(apiVersion).To(Equal("deckhouse.io/v1alpha2")) + Expect(apiVersion).To(Equal("virtualization.deckhouse.io/v1alpha2")) Expect(kind).To(Equal("VirtualMachineClass")) Expect(namespace).To(Equal("")) Expect(name).To(Equal(genericVMClassName)) From 0caaeaf16ac8358ca29ad98d6aaf5b5faa506f38 Mon Sep 17 00:00:00 2001 From: Pavel Tishkov Date: Tue, 21 Oct 2025 08:46:19 +0300 Subject: [PATCH 07/17] do not encode value Signed-off-by: Pavel Tishkov --- images/hooks/pkg/hooks/update-module-state/hook.go | 4 ++-- .../hooks/pkg/hooks/update-module-state/hook_test.go | 12 +++--------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/images/hooks/pkg/hooks/update-module-state/hook.go b/images/hooks/pkg/hooks/update-module-state/hook.go index 5cac8e64c0..73f621ca19 100644 --- a/images/hooks/pkg/hooks/update-module-state/hook.go +++ b/images/hooks/pkg/hooks/update-module-state/hook.go @@ -95,7 +95,7 @@ type ModuleState struct { func (ms ModuleState) ToSecretData() map[string][]byte { value := fmt.Sprintf("%t", ms.GenericVMClassCreated) return map[string][]byte{ - genericVMClassStateKey: []byte(base64.StdEncoding.EncodeToString([]byte(value))), + genericVMClassStateKey: []byte(value), } } @@ -103,7 +103,7 @@ func (ms ModuleState) ToPatchData() map[string]interface{} { value := fmt.Sprintf("%t", ms.GenericVMClassCreated) return map[string]interface{}{ "data": map[string]string{ - genericVMClassStateKey: base64.StdEncoding.EncodeToString([]byte(value)), + genericVMClassStateKey: value, }, } } diff --git a/images/hooks/pkg/hooks/update-module-state/hook_test.go b/images/hooks/pkg/hooks/update-module-state/hook_test.go index ae5f34f2e9..834670dfd3 100644 --- a/images/hooks/pkg/hooks/update-module-state/hook_test.go +++ b/images/hooks/pkg/hooks/update-module-state/hook_test.go @@ -78,9 +78,7 @@ var _ = Describe("Update Module State hook", func() { Expect(secret.Namespace).To(Equal(settings.ModuleNamespace)) Expect(secret.Data).To(HaveKey("generic-vmclass-created")) - decoded, err := base64.StdEncoding.DecodeString(string(secret.Data["generic-vmclass-created"])) - Expect(err).ToNot(HaveOccurred()) - Expect(string(decoded)).To(Equal("true")) + Expect(string(secret.Data["generic-vmclass-created"])).To(Equal("true")) }) patchCollector.PatchWithMergeMock.Optional() @@ -113,9 +111,7 @@ var _ = Describe("Update Module State hook", func() { Expect(ok).To(BeTrue()) Expect(data).To(HaveKey("generic-vmclass-created")) - decoded, err := base64.StdEncoding.DecodeString(data["generic-vmclass-created"]) - Expect(err).ToNot(HaveOccurred()) - Expect(string(decoded)).To(Equal("true")) + Expect(data["generic-vmclass-created"]).To(Equal("true")) }) patchCollector.CreateMock.Optional() @@ -163,9 +159,7 @@ var _ = Describe("Update Module State hook", func() { Expect(secret.Namespace).To(Equal(settings.ModuleNamespace)) Expect(secret.Data).To(HaveKey("generic-vmclass-created")) - decoded, err := base64.StdEncoding.DecodeString(string(secret.Data["generic-vmclass-created"])) - Expect(err).ToNot(HaveOccurred()) - Expect(string(decoded)).To(Equal("false")) + Expect(string(secret.Data["generic-vmclass-created"])).To(Equal("false")) }) patchCollector.PatchWithMergeMock.Optional() From 576270ac7b7b5d109587fb442d5b2ae7c5b067ad Mon Sep 17 00:00:00 2001 From: Pavel Tishkov Date: Tue, 21 Oct 2025 08:55:02 +0300 Subject: [PATCH 08/17] fix vmclass generic creation logic Signed-off-by: Pavel Tishkov --- .../pkg/hooks/create-generic-vmclass/hook.go | 41 +++++---- .../hooks/create-generic-vmclass/hook_test.go | 89 ++++++++++++++++++- .../pkg/hooks/update-module-state/hook.go | 30 +++---- .../hooks/update-module-state/hook_test.go | 35 +++++--- 4 files changed, 150 insertions(+), 45 deletions(-) diff --git a/images/hooks/pkg/hooks/create-generic-vmclass/hook.go b/images/hooks/pkg/hooks/create-generic-vmclass/hook.go index 0c6d584803..600040ddbb 100644 --- a/images/hooks/pkg/hooks/create-generic-vmclass/hook.go +++ b/images/hooks/pkg/hooks/create-generic-vmclass/hook.go @@ -87,26 +87,43 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { moduleStateSecrets := input.Snapshots.Get(moduleStateSecretSnapshot) vmClasses := input.Snapshots.Get(vmClassSnapshot) + vmClassExists := len(vmClasses) > 0 shouldCreateVMClass := false - if len(moduleStateSecrets) > 0 { + + if len(moduleStateSecrets) == 0 { + // Секрет отсутствует - создаем VMClass если его нет + shouldCreateVMClass = !vmClassExists + input.Logger.Info("Module-state secret doesn't exist, will create generic VirtualMachineClass if it doesn't exist") + } else { + // Секрет существует - проверяем его содержимое moduleStateData := make(map[string]interface{}) if err := moduleStateSecrets[0].UnmarshalTo(&moduleStateData); err == nil { if genericCreatedEncoded, exists := moduleStateData["generic-vmclass-created"]; exists { if encodedStr, ok := genericCreatedEncoded.(string); ok { if decodedBytes, err := base64.StdEncoding.DecodeString(encodedStr); err == nil { - if string(decodedBytes) == "true" { + genericCreated := string(decodedBytes) == "true" + if !genericCreated && !vmClassExists { + shouldCreateVMClass = true + input.Logger.Info("Module-state indicates generic VirtualMachineClass was not created, creating it") + } else if genericCreated && vmClassExists { + input.Logger.Info("Module-state correctly reflects that generic VirtualMachineClass exists") + } else if genericCreated && !vmClassExists { + input.Logger.Info("Module-state indicates generic VirtualMachineClass was created but it doesn't exist, will recreate it") shouldCreateVMClass = true - input.Logger.Info("Found record in module-state that generic VirtualMachineClass was created previously") + } else { + input.Logger.Info("Module-state correctly reflects that generic VirtualMachineClass doesn't exist") } } } + } else { + // Ключ отсутствует в секрете - создаем VMClass если его нет + shouldCreateVMClass = !vmClassExists + input.Logger.Info("Module-state secret doesn't contain generic-vmclass-created key, will create generic VirtualMachineClass if it doesn't exist") } } } - vmClassExists := len(vmClasses) > 0 - - if shouldCreateVMClass && !vmClassExists { + if shouldCreateVMClass { input.Logger.Info("Creating generic VirtualMachineClass") vmClass := &v1alpha2.VirtualMachineClass{ @@ -123,10 +140,8 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { }, Spec: v1alpha2.VirtualMachineClassSpec{ CPU: v1alpha2.CPU{ - Type: v1alpha2.CPUTypeModel, - Model: "Nehalem", - Features: nil, - Discovery: v1alpha2.CpuDiscovery{}, + Type: v1alpha2.CPUTypeModel, + Model: "Nehalem", }, SizingPolicies: []v1alpha2.SizingPolicy{ { @@ -166,11 +181,7 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { } input.PatchCollector.Create(vmClass) - input.Logger.Info("Generic VirtualMachineClass creation requested") - } else if shouldCreateVMClass && vmClassExists { - input.Logger.Info("Generic VirtualMachineClass already exists, no action needed") - } else if !shouldCreateVMClass && !vmClassExists { - input.Logger.Info("No record of generic VirtualMachineClass creation in module-state, skipping creation") + input.Logger.Info("VirtualMachineClass generic created") } return nil diff --git a/images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go b/images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go index acbcf11f9c..6dba622f67 100644 --- a/images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go +++ b/images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go @@ -75,7 +75,7 @@ var _ = Describe("Create Generic VMClass hook", func() { }) }) - It("should create generic vmclass when it doesn't exist", func() { + It("should recreate generic vmclass when it doesn't exist but state says it was created", func() { snapshots.GetMock.When(vmClassSnapshot).Then([]pkg.Snapshot{}) patchCollector.CreateMock.Set(func(obj interface{}) { @@ -109,9 +109,28 @@ var _ = Describe("Create Generic VMClass hook", func() { snapshots.GetMock.When(moduleStateSecretSnapshot).Then([]pkg.Snapshot{}) }) - It("should not create generic vmclass", func() { + It("should create generic vmclass when it doesn't exist", func() { snapshots.GetMock.When(vmClassSnapshot).Then([]pkg.Snapshot{}) + patchCollector.CreateMock.Set(func(obj interface{}) { + vmClass, ok := obj.(*v1alpha2.VirtualMachineClass) + Expect(ok).To(BeTrue()) + Expect(vmClass.Name).To(Equal("generic")) + Expect(vmClass.Labels).To(Equal(map[string]string{ + "app": "virtualization-controller", + "module": "virtualization", + })) + }) + + Expect(Reconcile(context.Background(), newInput())).To(Succeed()) + Expect(patchCollector.CreateMock.Calls()).To(HaveLen(1)) + }) + + It("should not create generic vmclass when it already exists", func() { + snapshots.GetMock.When(vmClassSnapshot).Then([]pkg.Snapshot{ + mock.NewSnapshotMock(GinkgoT()), + }) + patchCollector.CreateMock.Optional() Expect(Reconcile(context.Background(), newInput())).To(Succeed()) @@ -135,9 +154,73 @@ var _ = Describe("Create Generic VMClass hook", func() { }) }) - It("should not create generic vmclass", func() { + It("should create generic vmclass when it doesn't exist", func() { + snapshots.GetMock.When(vmClassSnapshot).Then([]pkg.Snapshot{}) + + patchCollector.CreateMock.Set(func(obj interface{}) { + vmClass, ok := obj.(*v1alpha2.VirtualMachineClass) + Expect(ok).To(BeTrue()) + Expect(vmClass.Name).To(Equal("generic")) + Expect(vmClass.Labels).To(Equal(map[string]string{ + "app": "virtualization-controller", + "module": "virtualization", + })) + }) + + Expect(Reconcile(context.Background(), newInput())).To(Succeed()) + Expect(patchCollector.CreateMock.Calls()).To(HaveLen(1)) + }) + + It("should not create generic vmclass when it already exists", func() { + snapshots.GetMock.When(vmClassSnapshot).Then([]pkg.Snapshot{ + mock.NewSnapshotMock(GinkgoT()), + }) + + patchCollector.CreateMock.Optional() + + Expect(Reconcile(context.Background(), newInput())).To(Succeed()) + Expect(patchCollector.CreateMock.Calls()).To(HaveLen(0)) + }) + }) + + Context("when module-state secret exists with generic-vmclass-created=false", func() { + BeforeEach(func() { + moduleStateData := map[string]interface{}{ + "generic-vmclass-created": base64.StdEncoding.EncodeToString([]byte("false")), + } + + snapshots.GetMock.When(moduleStateSecretSnapshot).Then([]pkg.Snapshot{ + mock.NewSnapshotMock(GinkgoT()).UnmarshalToMock.Set(func(v any) error { + data, ok := v.(*map[string]interface{}) + Expect(ok).To(BeTrue()) + *data = moduleStateData + return nil + }), + }) + }) + + It("should create generic vmclass when it doesn't exist", func() { snapshots.GetMock.When(vmClassSnapshot).Then([]pkg.Snapshot{}) + patchCollector.CreateMock.Set(func(obj interface{}) { + vmClass, ok := obj.(*v1alpha2.VirtualMachineClass) + Expect(ok).To(BeTrue()) + Expect(vmClass.Name).To(Equal("generic")) + Expect(vmClass.Labels).To(Equal(map[string]string{ + "app": "virtualization-controller", + "module": "virtualization", + })) + }) + + Expect(Reconcile(context.Background(), newInput())).To(Succeed()) + Expect(patchCollector.CreateMock.Calls()).To(HaveLen(1)) + }) + + It("should not create generic vmclass when it already exists", func() { + snapshots.GetMock.When(vmClassSnapshot).Then([]pkg.Snapshot{ + mock.NewSnapshotMock(GinkgoT()), + }) + patchCollector.CreateMock.Optional() Expect(Reconcile(context.Background(), newInput())).To(Succeed()) diff --git a/images/hooks/pkg/hooks/update-module-state/hook.go b/images/hooks/pkg/hooks/update-module-state/hook.go index 73f621ca19..f95d279a7e 100644 --- a/images/hooks/pkg/hooks/update-module-state/hook.go +++ b/images/hooks/pkg/hooks/update-module-state/hook.go @@ -115,7 +115,7 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { vmClassExists := len(vmClasses) > 0 needsUpdate := false - currentState := false + hasBeenCreated := false if len(moduleStateSecrets) > 0 { moduleStateData := make(map[string]interface{}) @@ -123,33 +123,31 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { if genericCreatedEncoded, exists := moduleStateData[genericVMClassStateKey]; exists { if encodedStr, ok := genericCreatedEncoded.(string); ok { if decodedBytes, err := base64.StdEncoding.DecodeString(encodedStr); err == nil { - currentState = string(decodedBytes) == "true" + hasBeenCreated = string(decodedBytes) == "true" } } } } } - if vmClassExists && !currentState { + // Записываем в секрет только когда VMClass создан и еще не записано + if vmClassExists && !hasBeenCreated { needsUpdate = true - input.Logger.Info("Generic VirtualMachineClass exists but module-state doesn't reflect this, updating secret") - } else if !vmClassExists && currentState { - input.Logger.Info("Generic VirtualMachineClass doesn't exist but module-state indicates it was created previously - keeping historical record") - } else if len(moduleStateSecrets) == 0 { - needsUpdate = true - input.Logger.Info("Module-state secret doesn't exist, creating it") - } else if vmClassExists && currentState { - input.Logger.Info("Module-state correctly reflects that generic VirtualMachineClass exists") - } else { - input.Logger.Info("Module-state correctly reflects that generic VirtualMachineClass doesn't exist") + input.Logger.Info("Generic VirtualMachineClass exists but module-state doesn't reflect it was created, updating secret") + } else if vmClassExists && hasBeenCreated { + input.Logger.Info("Module-state correctly reflects that generic VirtualMachineClass was created") + } else if !vmClassExists && hasBeenCreated { + input.Logger.Info("Generic VirtualMachineClass was created previously but doesn't exist now - keeping historical record") + } else if !vmClassExists && !hasBeenCreated { + input.Logger.Info("Generic VirtualMachineClass doesn't exist and was never created - no action needed") } if needsUpdate { - state := ModuleState{GenericVMClassCreated: vmClassExists} + state := ModuleState{GenericVMClassCreated: true} // Всегда записываем true, когда VMClass существует if len(moduleStateSecrets) > 0 { input.PatchCollector.PatchWithMerge(state.ToPatchData(), "v1", "Secret", settings.ModuleNamespace, moduleStateSecretName) - input.Logger.Info("Updated module-state secret") + input.Logger.Info("Updated module-state secret to record that generic VirtualMachineClass was created") } else { secret := &corev1.Secret{ TypeMeta: metav1.TypeMeta{ @@ -167,7 +165,7 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { Type: "Opaque", } input.PatchCollector.Create(secret) - input.Logger.Info("Created module-state secret") + input.Logger.Info("Created module-state secret to record that generic VirtualMachineClass was created") } } diff --git a/images/hooks/pkg/hooks/update-module-state/hook_test.go b/images/hooks/pkg/hooks/update-module-state/hook_test.go index 834670dfd3..16e3088f79 100644 --- a/images/hooks/pkg/hooks/update-module-state/hook_test.go +++ b/images/hooks/pkg/hooks/update-module-state/hook_test.go @@ -149,29 +149,42 @@ var _ = Describe("Update Module State hook", func() { snapshots.GetMock.When(vmClassSnapshot).Then([]pkg.Snapshot{}) }) - It("should create module-state secret with false value when it doesn't exist", func() { + It("should not create module-state secret when vmclass doesn't exist and secret doesn't exist", func() { snapshots.GetMock.When(moduleStateSecretSnapshot).Then([]pkg.Snapshot{}) - patchCollector.CreateMock.Set(func(obj interface{}) { - secret, ok := obj.(*corev1.Secret) - Expect(ok).To(BeTrue()) - Expect(secret.Name).To(Equal("module-state")) - Expect(secret.Namespace).To(Equal(settings.ModuleNamespace)) - Expect(secret.Data).To(HaveKey("generic-vmclass-created")) + patchCollector.CreateMock.Optional() + patchCollector.PatchWithMergeMock.Optional() - Expect(string(secret.Data["generic-vmclass-created"])).To(Equal("false")) + Expect(Reconcile(context.Background(), newInput())).To(Succeed()) + Expect(patchCollector.CreateMock.Calls()).To(HaveLen(0)) + Expect(patchCollector.PatchWithMergeMock.Calls()).To(HaveLen(0)) + }) + + It("should keep historical record when vmclass doesn't exist but module-state indicates it was created", func() { + moduleStateData := map[string]interface{}{ + "generic-vmclass-created": base64.StdEncoding.EncodeToString([]byte("true")), + } + + snapshots.GetMock.When(moduleStateSecretSnapshot).Then([]pkg.Snapshot{ + mock.NewSnapshotMock(GinkgoT()).UnmarshalToMock.Set(func(v any) error { + data, ok := v.(*map[string]interface{}) + Expect(ok).To(BeTrue()) + *data = moduleStateData + return nil + }), }) patchCollector.PatchWithMergeMock.Optional() + patchCollector.CreateMock.Optional() Expect(Reconcile(context.Background(), newInput())).To(Succeed()) - Expect(patchCollector.CreateMock.Calls()).To(HaveLen(1)) Expect(patchCollector.PatchWithMergeMock.Calls()).To(HaveLen(0)) + Expect(patchCollector.CreateMock.Calls()).To(HaveLen(0)) }) - It("should keep historical record when vmclass doesn't exist but module-state indicates it was created", func() { + It("should not update module-state secret when vmclass doesn't exist and secret contains false", func() { moduleStateData := map[string]interface{}{ - "generic-vmclass-created": base64.StdEncoding.EncodeToString([]byte("true")), + "generic-vmclass-created": base64.StdEncoding.EncodeToString([]byte("false")), } snapshots.GetMock.When(moduleStateSecretSnapshot).Then([]pkg.Snapshot{ From 1672b0762abb55f61b72dfc2113270ac7b4ce961 Mon Sep 17 00:00:00 2001 From: Pavel Tishkov Date: Wed, 22 Oct 2025 08:20:52 +0300 Subject: [PATCH 09/17] refactor Signed-off-by: Pavel Tishkov --- .../hooks/pkg/hooks/create-generic-vmclass/hook.go | 10 ---------- .../pkg/hooks/create-generic-vmclass/hook_test.go | 14 +++----------- images/hooks/pkg/hooks/update-module-state/hook.go | 2 +- 3 files changed, 4 insertions(+), 22 deletions(-) diff --git a/images/hooks/pkg/hooks/create-generic-vmclass/hook.go b/images/hooks/pkg/hooks/create-generic-vmclass/hook.go index 600040ddbb..123da85352 100644 --- a/images/hooks/pkg/hooks/create-generic-vmclass/hook.go +++ b/images/hooks/pkg/hooks/create-generic-vmclass/hook.go @@ -93,7 +93,6 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { if len(moduleStateSecrets) == 0 { // Секрет отсутствует - создаем VMClass если его нет shouldCreateVMClass = !vmClassExists - input.Logger.Info("Module-state secret doesn't exist, will create generic VirtualMachineClass if it doesn't exist") } else { // Секрет существует - проверяем его содержимое moduleStateData := make(map[string]interface{}) @@ -104,21 +103,12 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { genericCreated := string(decodedBytes) == "true" if !genericCreated && !vmClassExists { shouldCreateVMClass = true - input.Logger.Info("Module-state indicates generic VirtualMachineClass was not created, creating it") - } else if genericCreated && vmClassExists { - input.Logger.Info("Module-state correctly reflects that generic VirtualMachineClass exists") - } else if genericCreated && !vmClassExists { - input.Logger.Info("Module-state indicates generic VirtualMachineClass was created but it doesn't exist, will recreate it") - shouldCreateVMClass = true - } else { - input.Logger.Info("Module-state correctly reflects that generic VirtualMachineClass doesn't exist") } } } } else { // Ключ отсутствует в секрете - создаем VMClass если его нет shouldCreateVMClass = !vmClassExists - input.Logger.Info("Module-state secret doesn't contain generic-vmclass-created key, will create generic VirtualMachineClass if it doesn't exist") } } } diff --git a/images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go b/images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go index 6dba622f67..ce25d0566d 100644 --- a/images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go +++ b/images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go @@ -75,21 +75,13 @@ var _ = Describe("Create Generic VMClass hook", func() { }) }) - It("should recreate generic vmclass when it doesn't exist but state says it was created", func() { + It("should not recreate generic vmclass when it doesn't exist but state says it was created (user may have deleted it intentionally)", func() { snapshots.GetMock.When(vmClassSnapshot).Then([]pkg.Snapshot{}) - patchCollector.CreateMock.Set(func(obj interface{}) { - vmClass, ok := obj.(*v1alpha2.VirtualMachineClass) - Expect(ok).To(BeTrue()) - Expect(vmClass.Name).To(Equal("generic")) - Expect(vmClass.Labels).To(Equal(map[string]string{ - "app": "virtualization-controller", - "module": "virtualization", - })) - }) + patchCollector.CreateMock.Optional() Expect(Reconcile(context.Background(), newInput())).To(Succeed()) - Expect(patchCollector.CreateMock.Calls()).To(HaveLen(1)) + Expect(patchCollector.CreateMock.Calls()).To(HaveLen(0)) }) It("should not create generic vmclass when it already exists", func() { diff --git a/images/hooks/pkg/hooks/update-module-state/hook.go b/images/hooks/pkg/hooks/update-module-state/hook.go index f95d279a7e..f064fc04b7 100644 --- a/images/hooks/pkg/hooks/update-module-state/hook.go +++ b/images/hooks/pkg/hooks/update-module-state/hook.go @@ -137,7 +137,7 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { } else if vmClassExists && hasBeenCreated { input.Logger.Info("Module-state correctly reflects that generic VirtualMachineClass was created") } else if !vmClassExists && hasBeenCreated { - input.Logger.Info("Generic VirtualMachineClass was created previously but doesn't exist now - keeping historical record") + input.Logger.Info("Generic VirtualMachineClass was created previously but doesn't exist now - user may have deleted it intentionally, keeping historical record") } else if !vmClassExists && !hasBeenCreated { input.Logger.Info("Generic VirtualMachineClass doesn't exist and was never created - no action needed") } From 26ab58e10fdcb821b23a6007e8d3ba9e1babf9a3 Mon Sep 17 00:00:00 2001 From: Pavel Tishkov Date: Wed, 22 Oct 2025 08:36:32 +0300 Subject: [PATCH 10/17] refactor Signed-off-by: Pavel Tishkov --- .../pkg/hooks/create-generic-vmclass/hook.go | 134 +++++++++--------- .../hooks/create-generic-vmclass/hook_test.go | 40 ------ 2 files changed, 65 insertions(+), 109 deletions(-) diff --git a/images/hooks/pkg/hooks/create-generic-vmclass/hook.go b/images/hooks/pkg/hooks/create-generic-vmclass/hook.go index 123da85352..1233d663a3 100644 --- a/images/hooks/pkg/hooks/create-generic-vmclass/hook.go +++ b/images/hooks/pkg/hooks/create-generic-vmclass/hook.go @@ -87,92 +87,88 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { moduleStateSecrets := input.Snapshots.Get(moduleStateSecretSnapshot) vmClasses := input.Snapshots.Get(vmClassSnapshot) - vmClassExists := len(vmClasses) > 0 - shouldCreateVMClass := false - - if len(moduleStateSecrets) == 0 { - // Секрет отсутствует - создаем VMClass если его нет - shouldCreateVMClass = !vmClassExists - } else { - // Секрет существует - проверяем его содержимое + // nothing to do if generic vmclass already exists + if len(vmClasses) > 0 { + return nil + } + + // if module-state secret exists and contains generic-vmclass-created=true, nothing to do + if len(moduleStateSecrets) > 0 { moduleStateData := make(map[string]interface{}) - if err := moduleStateSecrets[0].UnmarshalTo(&moduleStateData); err == nil { - if genericCreatedEncoded, exists := moduleStateData["generic-vmclass-created"]; exists { - if encodedStr, ok := genericCreatedEncoded.(string); ok { - if decodedBytes, err := base64.StdEncoding.DecodeString(encodedStr); err == nil { - genericCreated := string(decodedBytes) == "true" - if !genericCreated && !vmClassExists { - shouldCreateVMClass = true - } + if err := moduleStateSecrets[0].UnmarshalTo(&moduleStateData); err != nil { + return err + } + + if genericCreatedEncoded, exists := moduleStateData["generic-vmclass-created"]; exists { + if encodedStr, ok := genericCreatedEncoded.(string); ok { + if decodedBytes, err := base64.StdEncoding.DecodeString(encodedStr); err == nil { + genericCreated := string(decodedBytes) == "true" + if genericCreated { + return nil } } - } else { - // Ключ отсутствует в секрете - создаем VMClass если его нет - shouldCreateVMClass = !vmClassExists } } } - if shouldCreateVMClass { - input.Logger.Info("Creating generic VirtualMachineClass") + input.Logger.Info("Creating generic VirtualMachineClass") - vmClass := &v1alpha2.VirtualMachineClass{ - TypeMeta: metav1.TypeMeta{ - APIVersion: apiVersion, - Kind: v1alpha2.VirtualMachineClassKind, + vmClass := &v1alpha2.VirtualMachineClass{ + TypeMeta: metav1.TypeMeta{ + APIVersion: apiVersion, + Kind: v1alpha2.VirtualMachineClassKind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: genericVMClassName, + Labels: map[string]string{ + "app": "virtualization-controller", + "module": settings.ModuleName, }, - ObjectMeta: metav1.ObjectMeta{ - Name: genericVMClassName, - Labels: map[string]string{ - "app": "virtualization-controller", - "module": settings.ModuleName, - }, + }, + Spec: v1alpha2.VirtualMachineClassSpec{ + CPU: v1alpha2.CPU{ + Type: v1alpha2.CPUTypeModel, + Model: "Nehalem", }, - Spec: v1alpha2.VirtualMachineClassSpec{ - CPU: v1alpha2.CPU{ - Type: v1alpha2.CPUTypeModel, - Model: "Nehalem", - }, - SizingPolicies: []v1alpha2.SizingPolicy{ - { - Cores: &v1alpha2.SizingPolicyCores{ - Min: 1, - Max: 4, - }, - DedicatedCores: []bool{false}, - CoreFractions: []v1alpha2.CoreFractionValue{5, 10, 20, 50, 100}, + SizingPolicies: []v1alpha2.SizingPolicy{ + { + Cores: &v1alpha2.SizingPolicyCores{ + Min: 1, + Max: 4, }, - { - Cores: &v1alpha2.SizingPolicyCores{ - Min: 5, - Max: 8, - }, - DedicatedCores: []bool{false}, - CoreFractions: []v1alpha2.CoreFractionValue{20, 50, 100}, + DedicatedCores: []bool{false}, + CoreFractions: []v1alpha2.CoreFractionValue{5, 10, 20, 50, 100}, + }, + { + Cores: &v1alpha2.SizingPolicyCores{ + Min: 5, + Max: 8, }, - { - Cores: &v1alpha2.SizingPolicyCores{ - Min: 9, - Max: 16, - }, - DedicatedCores: []bool{true, false}, - CoreFractions: []v1alpha2.CoreFractionValue{50, 100}, + DedicatedCores: []bool{false}, + CoreFractions: []v1alpha2.CoreFractionValue{20, 50, 100}, + }, + { + Cores: &v1alpha2.SizingPolicyCores{ + Min: 9, + Max: 16, }, - { - Cores: &v1alpha2.SizingPolicyCores{ - Min: 17, - Max: 1024, - }, - DedicatedCores: []bool{true, false}, - CoreFractions: []v1alpha2.CoreFractionValue{100}, + DedicatedCores: []bool{true, false}, + CoreFractions: []v1alpha2.CoreFractionValue{50, 100}, + }, + { + Cores: &v1alpha2.SizingPolicyCores{ + Min: 17, + Max: 1024, }, + DedicatedCores: []bool{true, false}, + CoreFractions: []v1alpha2.CoreFractionValue{100}, }, }, - } - - input.PatchCollector.Create(vmClass) - input.Logger.Info("VirtualMachineClass generic created") + }, } + input.PatchCollector.Create(vmClass) + input.Logger.Info("VirtualMachineClass generic created") + return nil } diff --git a/images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go b/images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go index ce25d0566d..a9d1c42742 100644 --- a/images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go +++ b/images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go @@ -84,16 +84,6 @@ var _ = Describe("Create Generic VMClass hook", func() { Expect(patchCollector.CreateMock.Calls()).To(HaveLen(0)) }) - It("should not create generic vmclass when it already exists", func() { - snapshots.GetMock.When(vmClassSnapshot).Then([]pkg.Snapshot{ - mock.NewSnapshotMock(GinkgoT()), - }) - - patchCollector.CreateMock.Optional() - - Expect(Reconcile(context.Background(), newInput())).To(Succeed()) - Expect(patchCollector.CreateMock.Calls()).To(HaveLen(0)) - }) }) Context("when module-state secret doesn't exist", func() { @@ -118,16 +108,6 @@ var _ = Describe("Create Generic VMClass hook", func() { Expect(patchCollector.CreateMock.Calls()).To(HaveLen(1)) }) - It("should not create generic vmclass when it already exists", func() { - snapshots.GetMock.When(vmClassSnapshot).Then([]pkg.Snapshot{ - mock.NewSnapshotMock(GinkgoT()), - }) - - patchCollector.CreateMock.Optional() - - Expect(Reconcile(context.Background(), newInput())).To(Succeed()) - Expect(patchCollector.CreateMock.Calls()).To(HaveLen(0)) - }) }) Context("when module-state secret exists but doesn't contain generic-vmclass-created", func() { @@ -163,16 +143,6 @@ var _ = Describe("Create Generic VMClass hook", func() { Expect(patchCollector.CreateMock.Calls()).To(HaveLen(1)) }) - It("should not create generic vmclass when it already exists", func() { - snapshots.GetMock.When(vmClassSnapshot).Then([]pkg.Snapshot{ - mock.NewSnapshotMock(GinkgoT()), - }) - - patchCollector.CreateMock.Optional() - - Expect(Reconcile(context.Background(), newInput())).To(Succeed()) - Expect(patchCollector.CreateMock.Calls()).To(HaveLen(0)) - }) }) Context("when module-state secret exists with generic-vmclass-created=false", func() { @@ -208,15 +178,5 @@ var _ = Describe("Create Generic VMClass hook", func() { Expect(patchCollector.CreateMock.Calls()).To(HaveLen(1)) }) - It("should not create generic vmclass when it already exists", func() { - snapshots.GetMock.When(vmClassSnapshot).Then([]pkg.Snapshot{ - mock.NewSnapshotMock(GinkgoT()), - }) - - patchCollector.CreateMock.Optional() - - Expect(Reconcile(context.Background(), newInput())).To(Succeed()) - Expect(patchCollector.CreateMock.Calls()).To(HaveLen(0)) - }) }) }) From c2b8739a5f1839acd96e89a3c333f7549a6f1e92 Mon Sep 17 00:00:00 2001 From: Pavel Tishkov Date: Wed, 22 Oct 2025 08:45:46 +0300 Subject: [PATCH 11/17] fix protector Signed-off-by: Pavel Tishkov --- .../hooks/prevent-default-vmclasses-deletion/hook.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/images/hooks/pkg/hooks/prevent-default-vmclasses-deletion/hook.go b/images/hooks/pkg/hooks/prevent-default-vmclasses-deletion/hook.go index 6ddff28143..620c1c2454 100644 --- a/images/hooks/pkg/hooks/prevent-default-vmclasses-deletion/hook.go +++ b/images/hooks/pkg/hooks/prevent-default-vmclasses-deletion/hook.go @@ -31,7 +31,7 @@ import ( ) const ( - removePassthroughHookName = "Prevent default VirtualMachineClasses deletion" + removePassthroughHookName = "Preserve VirtualMachineClasses on Helm uninstall" removePassthroughHookJQFilter = `.metadata` // see https://helm.sh/docs/howto/charts_tips_and_tricks/#tell-helm-not-to-uninstall-a-resource helmResourcePolicyKey = "helm.sh/resource-policy" @@ -73,6 +73,12 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { metadata := &metav1.ObjectMeta{} if err := vmc.UnmarshalTo(metadata); err != nil { input.Logger.Error(fmt.Sprintf("Failed to unmarshal metadata VirtualMachineClasses %v", err)) + continue + } + + // Skip if object is being deleted + if metadata.DeletionTimestamp != nil { + continue } policy := metadata.GetAnnotations()[helmResourcePolicyKey] @@ -93,7 +99,7 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { "value": helmResourcePolicyKeep, }, } - input.PatchCollector.JSONPatch(patch, apiVersion, v1alpha2.VirtualMachineClassKind, "", metadata.Name) + input.PatchCollector.PatchWithJSON(patch, apiVersion, v1alpha2.VirtualMachineClassKind, "", metadata.Name) input.Logger.Info(fmt.Sprintf("Added helm.sh/resource-policy=keep to VirtualMachineClass %s", metadata.Name)) } From 799316c0062dee5a4cbae6dd91b4b6880ade09b1 Mon Sep 17 00:00:00 2001 From: Pavel Tishkov Date: Wed, 22 Oct 2025 08:52:37 +0300 Subject: [PATCH 12/17] fix protector Signed-off-by: Pavel Tishkov --- .../virtualization-module-hooks/register.go | 1 - .../hook.go | 107 ------------------ .../pkg/hooks/update-module-state/hook.go | 13 +-- 3 files changed, 2 insertions(+), 119 deletions(-) delete mode 100644 images/hooks/pkg/hooks/prevent-default-vmclasses-deletion/hook.go diff --git a/images/hooks/cmd/virtualization-module-hooks/register.go b/images/hooks/cmd/virtualization-module-hooks/register.go index 49be3456f6..253d6896be 100644 --- a/images/hooks/cmd/virtualization-module-hooks/register.go +++ b/images/hooks/cmd/virtualization-module-hooks/register.go @@ -26,7 +26,6 @@ import ( _ "hooks/pkg/hooks/generate-secret-for-dvcr" _ "hooks/pkg/hooks/migrate-delete-renamed-validation-admission-policy" _ "hooks/pkg/hooks/migrate-virthandler-kvm-node-labels" - _ "hooks/pkg/hooks/prevent-default-vmclasses-deletion" _ "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/prevent-default-vmclasses-deletion/hook.go b/images/hooks/pkg/hooks/prevent-default-vmclasses-deletion/hook.go deleted file mode 100644 index 620c1c2454..0000000000 --- a/images/hooks/pkg/hooks/prevent-default-vmclasses-deletion/hook.go +++ /dev/null @@ -1,107 +0,0 @@ -/* -Copyright 2025 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 prevent_default_vmclasses_deletion - -import ( - "context" - "fmt" - - "hooks/pkg/settings" - - "github.com/deckhouse/virtualization/api/core" - "github.com/deckhouse/virtualization/api/core/v1alpha2" - - "github.com/deckhouse/module-sdk/pkg" - "github.com/deckhouse/module-sdk/pkg/registry" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -const ( - removePassthroughHookName = "Preserve VirtualMachineClasses on Helm uninstall" - removePassthroughHookJQFilter = `.metadata` - // see https://helm.sh/docs/howto/charts_tips_and_tricks/#tell-helm-not-to-uninstall-a-resource - helmResourcePolicyKey = "helm.sh/resource-policy" - helmResourcePolicyKeep = "keep" - apiVersion = core.GroupName + "/" + v1alpha2.Version -) - -var _ = registry.RegisterFunc(config, Reconcile) - -var config = &pkg.HookConfig{ - OnBeforeHelm: &pkg.OrderedConfig{Order: 10}, - Kubernetes: []pkg.KubernetesConfig{ - { - Name: removePassthroughHookName, - APIVersion: apiVersion, - Kind: v1alpha2.VirtualMachineClassKind, - JqFilter: removePassthroughHookJQFilter, - - LabelSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "module": settings.ModuleName, - }, - }, - }, - }, - - Queue: fmt.Sprintf("modules/%s", settings.ModuleName), -} - -func Reconcile(_ context.Context, input *pkg.HookInput) error { - vmcs := input.Snapshots.Get(removePassthroughHookName) - - if len(vmcs) == 0 { - input.Logger.Info("No VirtualMachineClasses found, nothing to do") - return nil - } - - for _, vmc := range vmcs { - metadata := &metav1.ObjectMeta{} - if err := vmc.UnmarshalTo(metadata); err != nil { - input.Logger.Error(fmt.Sprintf("Failed to unmarshal metadata VirtualMachineClasses %v", err)) - continue - } - - // Skip if object is being deleted - if metadata.DeletionTimestamp != nil { - continue - } - - policy := metadata.GetAnnotations()[helmResourcePolicyKey] - if policy == helmResourcePolicyKeep { - input.Logger.Info(fmt.Sprintf("VirtualMachineClass %s already has helm.sh/resource-policy=keep", metadata.Name)) - continue - } - - op := "add" - if policy != "" { - op = "replace" - input.Logger.Info(fmt.Sprintf("VirtualMachineClass %s has helm.sh/resource-policy=%s, will be replaced with helm.sh/resource-policy=keep", metadata.Name, policy)) - } - patch := []interface{}{ - map[string]string{ - "op": op, - "path": "/metadata/annotations/helm.sh~1resource-policy", - "value": helmResourcePolicyKeep, - }, - } - input.PatchCollector.PatchWithJSON(patch, apiVersion, v1alpha2.VirtualMachineClassKind, "", metadata.Name) - input.Logger.Info(fmt.Sprintf("Added helm.sh/resource-policy=keep to VirtualMachineClass %s", metadata.Name)) - } - - return nil -} diff --git a/images/hooks/pkg/hooks/update-module-state/hook.go b/images/hooks/pkg/hooks/update-module-state/hook.go index f064fc04b7..c0f5a5b900 100644 --- a/images/hooks/pkg/hooks/update-module-state/hook.go +++ b/images/hooks/pkg/hooks/update-module-state/hook.go @@ -130,24 +130,16 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { } } - // Записываем в секрет только когда VMClass создан и еще не записано + // Write to secret only when VMClass is created and not yet recorded if vmClassExists && !hasBeenCreated { needsUpdate = true - input.Logger.Info("Generic VirtualMachineClass exists but module-state doesn't reflect it was created, updating secret") - } else if vmClassExists && hasBeenCreated { - input.Logger.Info("Module-state correctly reflects that generic VirtualMachineClass was created") - } else if !vmClassExists && hasBeenCreated { - input.Logger.Info("Generic VirtualMachineClass was created previously but doesn't exist now - user may have deleted it intentionally, keeping historical record") - } else if !vmClassExists && !hasBeenCreated { - input.Logger.Info("Generic VirtualMachineClass doesn't exist and was never created - no action needed") } if needsUpdate { - state := ModuleState{GenericVMClassCreated: true} // Всегда записываем true, когда VMClass существует + state := ModuleState{GenericVMClassCreated: true} // Always write true when VMClass exists if len(moduleStateSecrets) > 0 { input.PatchCollector.PatchWithMerge(state.ToPatchData(), "v1", "Secret", settings.ModuleNamespace, moduleStateSecretName) - input.Logger.Info("Updated module-state secret to record that generic VirtualMachineClass was created") } else { secret := &corev1.Secret{ TypeMeta: metav1.TypeMeta{ @@ -165,7 +157,6 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { Type: "Opaque", } input.PatchCollector.Create(secret) - input.Logger.Info("Created module-state secret to record that generic VirtualMachineClass was created") } } From df4a79ae22d18c15fc7a465cbfde5cd6209f593e Mon Sep 17 00:00:00 2001 From: Pavel Tishkov Date: Wed, 22 Oct 2025 09:03:52 +0300 Subject: [PATCH 13/17] secret should be always created Signed-off-by: Pavel Tishkov --- .../pkg/hooks/update-module-state/hook.go | 52 ++++----- .../hooks/update-module-state/hook_test.go | 108 ++++++++++++++++-- 2 files changed, 120 insertions(+), 40 deletions(-) diff --git a/images/hooks/pkg/hooks/update-module-state/hook.go b/images/hooks/pkg/hooks/update-module-state/hook.go index c0f5a5b900..602582cbde 100644 --- a/images/hooks/pkg/hooks/update-module-state/hook.go +++ b/images/hooks/pkg/hooks/update-module-state/hook.go @@ -114,50 +114,46 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { vmClassExists := len(vmClasses) > 0 - needsUpdate := false - hasBeenCreated := false - + // Load existing state + currentState := ModuleState{GenericVMClassCreated: false} if len(moduleStateSecrets) > 0 { moduleStateData := make(map[string]interface{}) if err := moduleStateSecrets[0].UnmarshalTo(&moduleStateData); err == nil { if genericCreatedEncoded, exists := moduleStateData[genericVMClassStateKey]; exists { if encodedStr, ok := genericCreatedEncoded.(string); ok { if decodedBytes, err := base64.StdEncoding.DecodeString(encodedStr); err == nil { - hasBeenCreated = string(decodedBytes) == "true" + currentState.GenericVMClassCreated = string(decodedBytes) == "true" } } } } } - // Write to secret only when VMClass is created and not yet recorded - if vmClassExists && !hasBeenCreated { - needsUpdate = true + // Update state: generic-vmclass-created can only transition from false to true + newState := ModuleState{ + GenericVMClassCreated: currentState.GenericVMClassCreated || vmClassExists, } - if needsUpdate { - state := ModuleState{GenericVMClassCreated: true} // Always write true when VMClass exists - - if len(moduleStateSecrets) > 0 { - input.PatchCollector.PatchWithMerge(state.ToPatchData(), "v1", "Secret", settings.ModuleNamespace, moduleStateSecretName) - } else { - secret := &corev1.Secret{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "Secret", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: moduleStateSecretName, - Namespace: settings.ModuleNamespace, - Labels: map[string]string{ - "module": settings.ModuleName, - }, + // Always ensure secret exists with current state + if len(moduleStateSecrets) > 0 { + input.PatchCollector.PatchWithMerge(newState.ToPatchData(), "v1", "Secret", settings.ModuleNamespace, moduleStateSecretName) + } else { + secret := &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: moduleStateSecretName, + Namespace: settings.ModuleNamespace, + Labels: map[string]string{ + "module": settings.ModuleName, }, - Data: state.ToSecretData(), - Type: "Opaque", - } - input.PatchCollector.Create(secret) + }, + Data: newState.ToSecretData(), + Type: "Opaque", } + input.PatchCollector.Create(secret) } return nil diff --git a/images/hooks/pkg/hooks/update-module-state/hook_test.go b/images/hooks/pkg/hooks/update-module-state/hook_test.go index 16e3088f79..7ced522e6a 100644 --- a/images/hooks/pkg/hooks/update-module-state/hook_test.go +++ b/images/hooks/pkg/hooks/update-module-state/hook_test.go @@ -121,7 +121,7 @@ var _ = Describe("Update Module State hook", func() { Expect(patchCollector.CreateMock.Calls()).To(HaveLen(0)) }) - It("should not update module-state secret when it has correct value", func() { + It("should update module-state secret even when it has correct value", func() { moduleStateData := map[string]interface{}{ "generic-vmclass-created": base64.StdEncoding.EncodeToString([]byte("true")), } @@ -135,12 +135,23 @@ var _ = Describe("Update Module State hook", func() { }), }) + patchCollector.PatchWithMergeMock.Set(func(obj interface{}, apiVersion, kind, namespace, name string, opts ...pkg.PatchCollectorOption) { + patchData, ok := obj.(map[string]interface{}) + Expect(ok).To(BeTrue()) + Expect(patchData).To(HaveKey("data")) + + data, ok := patchData["data"].(map[string]string) + Expect(ok).To(BeTrue()) + Expect(data).To(HaveKey("generic-vmclass-created")) + + Expect(data["generic-vmclass-created"]).To(Equal("true")) + }) + patchCollector.CreateMock.Optional() - patchCollector.PatchWithMergeMock.Optional() Expect(Reconcile(context.Background(), newInput())).To(Succeed()) + Expect(patchCollector.PatchWithMergeMock.Calls()).To(HaveLen(1)) Expect(patchCollector.CreateMock.Calls()).To(HaveLen(0)) - Expect(patchCollector.PatchWithMergeMock.Calls()).To(HaveLen(0)) }) }) @@ -149,18 +160,27 @@ var _ = Describe("Update Module State hook", func() { snapshots.GetMock.When(vmClassSnapshot).Then([]pkg.Snapshot{}) }) - It("should not create module-state secret when vmclass doesn't exist and secret doesn't exist", func() { + It("should create module-state secret even when vmclass doesn't exist and secret doesn't exist", func() { snapshots.GetMock.When(moduleStateSecretSnapshot).Then([]pkg.Snapshot{}) - patchCollector.CreateMock.Optional() + patchCollector.CreateMock.Set(func(obj interface{}) { + secret, ok := obj.(*corev1.Secret) + Expect(ok).To(BeTrue()) + Expect(secret.Name).To(Equal("module-state")) + Expect(secret.Namespace).To(Equal(settings.ModuleNamespace)) + Expect(secret.Data).To(HaveKey("generic-vmclass-created")) + + Expect(string(secret.Data["generic-vmclass-created"])).To(Equal("false")) + }) + patchCollector.PatchWithMergeMock.Optional() Expect(Reconcile(context.Background(), newInput())).To(Succeed()) - Expect(patchCollector.CreateMock.Calls()).To(HaveLen(0)) + Expect(patchCollector.CreateMock.Calls()).To(HaveLen(1)) Expect(patchCollector.PatchWithMergeMock.Calls()).To(HaveLen(0)) }) - It("should keep historical record when vmclass doesn't exist but module-state indicates it was created", func() { + It("should update module-state secret and keep historical record when vmclass doesn't exist but module-state indicates it was created", func() { moduleStateData := map[string]interface{}{ "generic-vmclass-created": base64.StdEncoding.EncodeToString([]byte("true")), } @@ -174,15 +194,27 @@ var _ = Describe("Update Module State hook", func() { }), }) - patchCollector.PatchWithMergeMock.Optional() + patchCollector.PatchWithMergeMock.Set(func(obj interface{}, apiVersion, kind, namespace, name string, opts ...pkg.PatchCollectorOption) { + patchData, ok := obj.(map[string]interface{}) + Expect(ok).To(BeTrue()) + Expect(patchData).To(HaveKey("data")) + + data, ok := patchData["data"].(map[string]string) + Expect(ok).To(BeTrue()) + Expect(data).To(HaveKey("generic-vmclass-created")) + + // Should keep historical record (true) even though VMClass doesn't exist now + Expect(data["generic-vmclass-created"]).To(Equal("true")) + }) + patchCollector.CreateMock.Optional() Expect(Reconcile(context.Background(), newInput())).To(Succeed()) - Expect(patchCollector.PatchWithMergeMock.Calls()).To(HaveLen(0)) + Expect(patchCollector.PatchWithMergeMock.Calls()).To(HaveLen(1)) Expect(patchCollector.CreateMock.Calls()).To(HaveLen(0)) }) - It("should not update module-state secret when vmclass doesn't exist and secret contains false", func() { + It("should update module-state secret when vmclass doesn't exist and secret contains false", func() { moduleStateData := map[string]interface{}{ "generic-vmclass-created": base64.StdEncoding.EncodeToString([]byte("false")), } @@ -196,11 +228,63 @@ var _ = Describe("Update Module State hook", func() { }), }) - patchCollector.PatchWithMergeMock.Optional() + patchCollector.PatchWithMergeMock.Set(func(obj interface{}, apiVersion, kind, namespace, name string, opts ...pkg.PatchCollectorOption) { + patchData, ok := obj.(map[string]interface{}) + Expect(ok).To(BeTrue()) + Expect(patchData).To(HaveKey("data")) + + data, ok := patchData["data"].(map[string]string) + Expect(ok).To(BeTrue()) + Expect(data).To(HaveKey("generic-vmclass-created")) + + // Should remain false since VMClass doesn't exist + Expect(data["generic-vmclass-created"]).To(Equal("false")) + }) + patchCollector.CreateMock.Optional() Expect(Reconcile(context.Background(), newInput())).To(Succeed()) - Expect(patchCollector.PatchWithMergeMock.Calls()).To(HaveLen(0)) + Expect(patchCollector.PatchWithMergeMock.Calls()).To(HaveLen(1)) + Expect(patchCollector.CreateMock.Calls()).To(HaveLen(0)) + }) + }) + + Context("state transition logic", func() { + It("should preserve historical true value even when vmclass is deleted and recreated", func() { + // First, simulate that VMClass was created and state recorded as true + moduleStateData := map[string]interface{}{ + "generic-vmclass-created": base64.StdEncoding.EncodeToString([]byte("true")), + } + + // VMClass doesn't exist now (was deleted) + snapshots.GetMock.When(vmClassSnapshot).Then([]pkg.Snapshot{}) + + snapshots.GetMock.When(moduleStateSecretSnapshot).Then([]pkg.Snapshot{ + mock.NewSnapshotMock(GinkgoT()).UnmarshalToMock.Set(func(v any) error { + data, ok := v.(*map[string]interface{}) + Expect(ok).To(BeTrue()) + *data = moduleStateData + return nil + }), + }) + + patchCollector.PatchWithMergeMock.Set(func(obj interface{}, apiVersion, kind, namespace, name string, opts ...pkg.PatchCollectorOption) { + patchData, ok := obj.(map[string]interface{}) + Expect(ok).To(BeTrue()) + Expect(patchData).To(HaveKey("data")) + + data, ok := patchData["data"].(map[string]string) + Expect(ok).To(BeTrue()) + Expect(data).To(HaveKey("generic-vmclass-created")) + + // Should preserve historical true value even though VMClass doesn't exist + Expect(data["generic-vmclass-created"]).To(Equal("true")) + }) + + patchCollector.CreateMock.Optional() + + Expect(Reconcile(context.Background(), newInput())).To(Succeed()) + Expect(patchCollector.PatchWithMergeMock.Calls()).To(HaveLen(1)) Expect(patchCollector.CreateMock.Calls()).To(HaveLen(0)) }) }) From 2e9574851bd26a72f811ea187e7364bf40d306cc Mon Sep 17 00:00:00 2001 From: Pavel Tishkov Date: Wed, 22 Oct 2025 09:14:06 +0300 Subject: [PATCH 14/17] fix base64 enc Signed-off-by: Pavel Tishkov --- images/hooks/pkg/hooks/update-module-state/hook.go | 2 +- .../hooks/pkg/hooks/update-module-state/hook_test.go | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/images/hooks/pkg/hooks/update-module-state/hook.go b/images/hooks/pkg/hooks/update-module-state/hook.go index 602582cbde..1851edb2e9 100644 --- a/images/hooks/pkg/hooks/update-module-state/hook.go +++ b/images/hooks/pkg/hooks/update-module-state/hook.go @@ -103,7 +103,7 @@ func (ms ModuleState) ToPatchData() map[string]interface{} { value := fmt.Sprintf("%t", ms.GenericVMClassCreated) return map[string]interface{}{ "data": map[string]string{ - genericVMClassStateKey: value, + genericVMClassStateKey: base64.StdEncoding.EncodeToString([]byte(value)), }, } } diff --git a/images/hooks/pkg/hooks/update-module-state/hook_test.go b/images/hooks/pkg/hooks/update-module-state/hook_test.go index 7ced522e6a..f7c8304471 100644 --- a/images/hooks/pkg/hooks/update-module-state/hook_test.go +++ b/images/hooks/pkg/hooks/update-module-state/hook_test.go @@ -111,7 +111,7 @@ var _ = Describe("Update Module State hook", func() { Expect(ok).To(BeTrue()) Expect(data).To(HaveKey("generic-vmclass-created")) - Expect(data["generic-vmclass-created"]).To(Equal("true")) + Expect(data["generic-vmclass-created"]).To(Equal(base64.StdEncoding.EncodeToString([]byte("true")))) }) patchCollector.CreateMock.Optional() @@ -144,7 +144,7 @@ var _ = Describe("Update Module State hook", func() { Expect(ok).To(BeTrue()) Expect(data).To(HaveKey("generic-vmclass-created")) - Expect(data["generic-vmclass-created"]).To(Equal("true")) + Expect(data["generic-vmclass-created"]).To(Equal(base64.StdEncoding.EncodeToString([]byte("true")))) }) patchCollector.CreateMock.Optional() @@ -204,7 +204,7 @@ var _ = Describe("Update Module State hook", func() { Expect(data).To(HaveKey("generic-vmclass-created")) // Should keep historical record (true) even though VMClass doesn't exist now - Expect(data["generic-vmclass-created"]).To(Equal("true")) + Expect(data["generic-vmclass-created"]).To(Equal(base64.StdEncoding.EncodeToString([]byte("true")))) }) patchCollector.CreateMock.Optional() @@ -238,7 +238,7 @@ var _ = Describe("Update Module State hook", func() { Expect(data).To(HaveKey("generic-vmclass-created")) // Should remain false since VMClass doesn't exist - Expect(data["generic-vmclass-created"]).To(Equal("false")) + Expect(data["generic-vmclass-created"]).To(Equal(base64.StdEncoding.EncodeToString([]byte("false")))) }) patchCollector.CreateMock.Optional() @@ -278,7 +278,7 @@ var _ = Describe("Update Module State hook", func() { Expect(data).To(HaveKey("generic-vmclass-created")) // Should preserve historical true value even though VMClass doesn't exist - Expect(data["generic-vmclass-created"]).To(Equal("true")) + Expect(data["generic-vmclass-created"]).To(Equal(base64.StdEncoding.EncodeToString([]byte("true")))) }) patchCollector.CreateMock.Optional() From ef8f924e93aac04a23a8a4321464538d30b2dea0 Mon Sep 17 00:00:00 2001 From: Pavel Tishkov Date: Fri, 31 Oct 2025 21:53:24 +0300 Subject: [PATCH 15/17] chore(module): remove annotations --- .../hook.go | 44 ++++++++--- .../hook_test.go | 78 ++++++++++++++++++- 2 files changed, 108 insertions(+), 14 deletions(-) diff --git a/images/hooks/pkg/hooks/drop-helm-labels-from-generic-vmclass/hook.go b/images/hooks/pkg/hooks/drop-helm-labels-from-generic-vmclass/hook.go index c9f6e4c571..23185bd5b2 100644 --- a/images/hooks/pkg/hooks/drop-helm-labels-from-generic-vmclass/hook.go +++ b/images/hooks/pkg/hooks/drop-helm-labels-from-generic-vmclass/hook.go @@ -36,8 +36,10 @@ const ( ) const ( - helmManagedByLabel = "app.kubernetes.io/managed-by" - helmHeritageLabel = "heritage" + helmManagedByLabel = "app.kubernetes.io/managed-by" + helmHeritageLabel = "heritage" + helmReleaseNameAnno = "meta.helm.sh/release-name" + helmReleaseNamespaceAnno = "meta.helm.sh/release-namespace" ) var _ = registry.RegisterFunc(configDropHelmLabels, handlerDropHelmLabels) @@ -49,7 +51,7 @@ var configDropHelmLabels = &pkg.HookConfig{ Name: vmClassSnapshot, APIVersion: "virtualization.deckhouse.io/v1alpha2", Kind: v1alpha2.VirtualMachineClassKind, - JqFilter: ".metadata", + JqFilter: "{name: .metadata.name, labels: .metadata.labels, annotations: .metadata.annotations}", NameSelector: &pkg.NameSelector{ MatchNames: []string{genericVMClassName}, }, @@ -69,8 +71,9 @@ var configDropHelmLabels = &pkg.HookConfig{ } type VMClassMetadata struct { - Name string `json:"name"` - Labels map[string]string `json:"labels"` + Name string `json:"name"` + Labels map[string]string `json:"labels"` + Annotations map[string]string `json:"annotations"` } func handlerDropHelmLabels(_ context.Context, input *pkg.HookInput) error { @@ -100,7 +103,7 @@ func handlerDropHelmLabels(_ context.Context, input *pkg.HookInput) error { } var patches []map[string]interface{} - hasLabelsToRemove := false + hasChanges := false // Check and prepare patches for Helm labels if _, exists := vmClass.Labels[helmManagedByLabel]; exists { @@ -109,7 +112,7 @@ func handlerDropHelmLabels(_ context.Context, input *pkg.HookInput) error { "path": fmt.Sprintf("/metadata/labels/%s", jsonPatchEscape(helmManagedByLabel)), "value": nil, }) - hasLabelsToRemove = true + hasChanges = true } if _, exists := vmClass.Labels[helmHeritageLabel]; exists { @@ -118,14 +121,35 @@ func handlerDropHelmLabels(_ context.Context, input *pkg.HookInput) error { "path": fmt.Sprintf("/metadata/labels/%s", jsonPatchEscape(helmHeritageLabel)), "value": nil, }) - hasLabelsToRemove = true + hasChanges = true } - if !hasLabelsToRemove { + // Check and prepare patches for Helm annotations + if vmClass.Annotations != nil { + if releaseName, exists := vmClass.Annotations[helmReleaseNameAnno]; exists && releaseName == settings.ModuleName { + patches = append(patches, map[string]interface{}{ + "op": "remove", + "path": fmt.Sprintf("/metadata/annotations/%s", jsonPatchEscape(helmReleaseNameAnno)), + "value": nil, + }) + hasChanges = true + } + + if releaseNamespace, exists := vmClass.Annotations[helmReleaseNamespaceAnno]; exists && releaseNamespace == settings.ModuleNamespace { + patches = append(patches, map[string]interface{}{ + "op": "remove", + "path": fmt.Sprintf("/metadata/annotations/%s", jsonPatchEscape(helmReleaseNamespaceAnno)), + "value": nil, + }) + hasChanges = true + } + } + + if !hasChanges { return nil } - input.Logger.Info("Removing Helm labels from generic VMClass") + input.Logger.Info("Removing Helm labels and annotations from generic VMClass") input.PatchCollector.PatchWithJSON( patches, "virtualization.deckhouse.io/v1alpha2", diff --git a/images/hooks/pkg/hooks/drop-helm-labels-from-generic-vmclass/hook_test.go b/images/hooks/pkg/hooks/drop-helm-labels-from-generic-vmclass/hook_test.go index 673e2a0db5..200be33583 100644 --- a/images/hooks/pkg/hooks/drop-helm-labels-from-generic-vmclass/hook_test.go +++ b/images/hooks/pkg/hooks/drop-helm-labels-from-generic-vmclass/hook_test.go @@ -47,7 +47,7 @@ var _ = Describe("Drop Helm labels from generic VMClass", func() { } } - newSnapshot := func(withManagedBy, withHeritage bool) pkg.Snapshot { + newSnapshot := func(withManagedBy, withHeritage bool, withAnnotations bool) pkg.Snapshot { return mock.NewSnapshotMock(GinkgoT()).UnmarshalToMock.Set(func(v any) (err error) { obj, ok := v.(*VMClassMetadata) Expect(ok).To(BeTrue()) @@ -65,6 +65,12 @@ var _ = Describe("Drop Helm labels from generic VMClass", func() { obj.Labels[helmHeritageLabel] = "deckhouse" } + if withAnnotations { + obj.Annotations = make(map[string]string) + obj.Annotations[helmReleaseNameAnno] = "virtualization" + obj.Annotations[helmReleaseNamespaceAnno] = "d8-virtualization" + } + return nil }) } @@ -78,8 +84,8 @@ var _ = Describe("Drop Helm labels from generic VMClass", func() { patchCollector = mock.NewPatchCollectorMock(GinkgoT()) }) - It("Should drop both Helm labels from generic VMClass with all required labels", func() { - setSnapshots(newSnapshot(true, true)) + It("Should drop both Helm labels and annotations from generic VMClass with all required labels", func() { + setSnapshots(newSnapshot(true, true, true)) patchCollector.PatchWithJSONMock.Set(func(patch any, apiVersion, kind, namespace, name string, opts ...pkg.PatchCollectorOption) { Expect(apiVersion).To(Equal("virtualization.deckhouse.io/v1alpha2")) Expect(kind).To(Equal("VirtualMachineClass")) @@ -89,7 +95,7 @@ var _ = Describe("Drop Helm labels from generic VMClass", func() { jsonPatch, ok := patch.([]map[string]interface{}) Expect(ok).To(BeTrue()) - Expect(jsonPatch).To(HaveLen(2)) + Expect(jsonPatch).To(HaveLen(4)) // Check first patch (managed-by label) Expect(jsonPatch[0]["op"]).To(Equal("remove")) @@ -100,6 +106,16 @@ var _ = Describe("Drop Helm labels from generic VMClass", func() { Expect(jsonPatch[1]["op"]).To(Equal("remove")) Expect(jsonPatch[1]["path"]).To(Equal(fmt.Sprintf("/metadata/labels/%s", jsonPatchEscape(helmHeritageLabel)))) Expect(jsonPatch[1]["value"]).To(BeNil()) + + // Check third patch (release-name annotation) + Expect(jsonPatch[2]["op"]).To(Equal("remove")) + Expect(jsonPatch[2]["path"]).To(Equal(fmt.Sprintf("/metadata/annotations/%s", jsonPatchEscape(helmReleaseNameAnno)))) + Expect(jsonPatch[2]["value"]).To(BeNil()) + + // Check fourth patch (release-namespace annotation) + Expect(jsonPatch[3]["op"]).To(Equal("remove")) + Expect(jsonPatch[3]["path"]).To(Equal(fmt.Sprintf("/metadata/annotations/%s", jsonPatchEscape(helmReleaseNamespaceAnno)))) + Expect(jsonPatch[3]["value"]).To(BeNil()) }) Expect(handlerDropHelmLabels(context.Background(), newInput())).To(Succeed()) @@ -125,6 +141,60 @@ var _ = Describe("Drop Helm labels from generic VMClass", func() { Expect(handlerDropHelmLabels(context.Background(), newInput())).To(Succeed()) }) + It("Should drop only labels when annotations are missing", func() { + setSnapshots(newSnapshot(true, true, false)) + patchCollector.PatchWithJSONMock.Set(func(patch any, apiVersion, kind, namespace, name string, opts ...pkg.PatchCollectorOption) { + jsonPatch, ok := patch.([]map[string]interface{}) + Expect(ok).To(BeTrue()) + Expect(jsonPatch).To(HaveLen(2)) + + // Check first patch (managed-by label) + Expect(jsonPatch[0]["op"]).To(Equal("remove")) + Expect(jsonPatch[0]["path"]).To(Equal(fmt.Sprintf("/metadata/labels/%s", jsonPatchEscape(helmManagedByLabel)))) + + // Check second patch (heritage label) + Expect(jsonPatch[1]["op"]).To(Equal("remove")) + Expect(jsonPatch[1]["path"]).To(Equal(fmt.Sprintf("/metadata/labels/%s", jsonPatchEscape(helmHeritageLabel)))) + }) + + Expect(handlerDropHelmLabels(context.Background(), newInput())).To(Succeed()) + }) + + It("Should not drop annotations with wrong values", func() { + wrongAnnotationSnapshot := mock.NewSnapshotMock(GinkgoT()).UnmarshalToMock.Set(func(v any) (err error) { + obj, ok := v.(*VMClassMetadata) + Expect(ok).To(BeTrue()) + obj.Name = genericVMClassName + obj.Labels = make(map[string]string) + + // Required labels for VMClass to be found by the hook + obj.Labels["app"] = "virtualization-controller" + obj.Labels["module"] = "virtualization" + obj.Labels[helmManagedByLabel] = "Helm" + obj.Labels[helmHeritageLabel] = "deckhouse" + + // Annotations with wrong values - should not be removed + obj.Annotations = make(map[string]string) + obj.Annotations[helmReleaseNameAnno] = "wrong-module-name" + obj.Annotations[helmReleaseNamespaceAnno] = "wrong-namespace" + + return nil + }) + + setSnapshots(wrongAnnotationSnapshot) + patchCollector.PatchWithJSONMock.Set(func(patch any, apiVersion, kind, namespace, name string, opts ...pkg.PatchCollectorOption) { + jsonPatch, ok := patch.([]map[string]interface{}) + Expect(ok).To(BeTrue()) + Expect(jsonPatch).To(HaveLen(2)) + + // Only labels should be removed, not annotations with wrong values + Expect(jsonPatch[0]["path"]).To(ContainSubstring("/metadata/labels/")) + Expect(jsonPatch[1]["path"]).To(ContainSubstring("/metadata/labels/")) + }) + + Expect(handlerDropHelmLabels(context.Background(), newInput())).To(Succeed()) + }) + It("Should do nothing when VMClass not found", func() { setSnapshots() Expect(handlerDropHelmLabels(context.Background(), newInput())).To(Succeed()) From 74e2190535619dd48b9d5dff636868875462f85a Mon Sep 17 00:00:00 2001 From: Pavel Tishkov Date: Fri, 31 Oct 2025 21:56:57 +0300 Subject: [PATCH 16/17] chore(module): fix secret encoding --- .../pkg/hooks/create-generic-vmclass/hook.go | 19 ++--- .../hooks/create-generic-vmclass/hook_test.go | 45 ++++++++---- .../pkg/hooks/update-module-state/hook.go | 14 ++-- .../hooks/update-module-state/hook_test.go | 71 +++++++++++++------ 4 files changed, 94 insertions(+), 55 deletions(-) diff --git a/images/hooks/pkg/hooks/create-generic-vmclass/hook.go b/images/hooks/pkg/hooks/create-generic-vmclass/hook.go index 1233d663a3..2cc9a94c7d 100644 --- a/images/hooks/pkg/hooks/create-generic-vmclass/hook.go +++ b/images/hooks/pkg/hooks/create-generic-vmclass/hook.go @@ -17,7 +17,6 @@ package create_generic_vmclass import ( "context" - "encoding/base64" "fmt" "hooks/pkg/settings" @@ -28,6 +27,7 @@ import ( "github.com/deckhouse/module-sdk/pkg" "github.com/deckhouse/module-sdk/pkg/registry" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" ) @@ -51,7 +51,7 @@ var config = &pkg.HookConfig{ Name: moduleStateSecretSnapshot, APIVersion: "v1", Kind: "Secret", - JqFilter: `.data`, + JqFilter: `{"metadata": .metadata, "data": .data}`, NameSelector: &pkg.NameSelector{ MatchNames: []string{moduleStateSecretName}, }, @@ -94,20 +94,13 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { // if module-state secret exists and contains generic-vmclass-created=true, nothing to do if len(moduleStateSecrets) > 0 { - moduleStateData := make(map[string]interface{}) - if err := moduleStateSecrets[0].UnmarshalTo(&moduleStateData); err != nil { + var moduleStateSecret corev1.Secret + if err := moduleStateSecrets[0].UnmarshalTo(&moduleStateSecret); err != nil { return err } - if genericCreatedEncoded, exists := moduleStateData["generic-vmclass-created"]; exists { - if encodedStr, ok := genericCreatedEncoded.(string); ok { - if decodedBytes, err := base64.StdEncoding.DecodeString(encodedStr); err == nil { - genericCreated := string(decodedBytes) == "true" - if genericCreated { - return nil - } - } - } + if string(moduleStateSecret.Data["generic-vmclass-created"]) == "true" { + return nil } } diff --git a/images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go b/images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go index a9d1c42742..b8dcc271c0 100644 --- a/images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go +++ b/images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go @@ -18,7 +18,6 @@ package create_generic_vmclass import ( "context" - "encoding/base64" "testing" . "github.com/onsi/ginkgo/v2" @@ -28,6 +27,8 @@ import ( "github.com/deckhouse/module-sdk/pkg" "github.com/deckhouse/module-sdk/testing/mock" "github.com/deckhouse/virtualization/api/core/v1alpha2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestCreateGenericVMClass(t *testing.T) { @@ -61,15 +62,21 @@ var _ = Describe("Create Generic VMClass hook", func() { Context("when module-state secret exists with generic-vmclass-created=true", func() { BeforeEach(func() { - moduleStateData := map[string]interface{}{ - "generic-vmclass-created": base64.StdEncoding.EncodeToString([]byte("true")), + moduleStateSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "module-state", + Namespace: "d8-virtualization", + }, + Data: map[string][]byte{ + "generic-vmclass-created": []byte("true"), + }, } snapshots.GetMock.When(moduleStateSecretSnapshot).Then([]pkg.Snapshot{ mock.NewSnapshotMock(GinkgoT()).UnmarshalToMock.Set(func(v any) error { - data, ok := v.(*map[string]interface{}) + secret, ok := v.(*corev1.Secret) Expect(ok).To(BeTrue()) - *data = moduleStateData + *secret = *moduleStateSecret return nil }), }) @@ -112,15 +119,21 @@ var _ = Describe("Create Generic VMClass hook", func() { Context("when module-state secret exists but doesn't contain generic-vmclass-created", func() { BeforeEach(func() { - moduleStateData := map[string]interface{}{ - "other-key": base64.StdEncoding.EncodeToString([]byte("other-value")), + moduleStateSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "module-state", + Namespace: "d8-virtualization", + }, + Data: map[string][]byte{ + "other-key": []byte("other-value"), + }, } snapshots.GetMock.When(moduleStateSecretSnapshot).Then([]pkg.Snapshot{ mock.NewSnapshotMock(GinkgoT()).UnmarshalToMock.Set(func(v any) error { - data, ok := v.(*map[string]interface{}) + secret, ok := v.(*corev1.Secret) Expect(ok).To(BeTrue()) - *data = moduleStateData + *secret = *moduleStateSecret return nil }), }) @@ -147,15 +160,21 @@ var _ = Describe("Create Generic VMClass hook", func() { Context("when module-state secret exists with generic-vmclass-created=false", func() { BeforeEach(func() { - moduleStateData := map[string]interface{}{ - "generic-vmclass-created": base64.StdEncoding.EncodeToString([]byte("false")), + moduleStateSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "module-state", + Namespace: "d8-virtualization", + }, + Data: map[string][]byte{ + "generic-vmclass-created": []byte("false"), + }, } snapshots.GetMock.When(moduleStateSecretSnapshot).Then([]pkg.Snapshot{ mock.NewSnapshotMock(GinkgoT()).UnmarshalToMock.Set(func(v any) error { - data, ok := v.(*map[string]interface{}) + secret, ok := v.(*corev1.Secret) Expect(ok).To(BeTrue()) - *data = moduleStateData + *secret = *moduleStateSecret return nil }), }) diff --git a/images/hooks/pkg/hooks/update-module-state/hook.go b/images/hooks/pkg/hooks/update-module-state/hook.go index 1851edb2e9..82d1000acc 100644 --- a/images/hooks/pkg/hooks/update-module-state/hook.go +++ b/images/hooks/pkg/hooks/update-module-state/hook.go @@ -72,7 +72,7 @@ var config = &pkg.HookConfig{ Name: moduleStateSecretSnapshot, APIVersion: "v1", Kind: "Secret", - JqFilter: `.data`, + JqFilter: `{"metadata": .metadata, "data": .data}`, NameSelector: &pkg.NameSelector{ MatchNames: []string{moduleStateSecretName}, }, @@ -117,14 +117,10 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { // Load existing state currentState := ModuleState{GenericVMClassCreated: false} if len(moduleStateSecrets) > 0 { - moduleStateData := make(map[string]interface{}) - if err := moduleStateSecrets[0].UnmarshalTo(&moduleStateData); err == nil { - if genericCreatedEncoded, exists := moduleStateData[genericVMClassStateKey]; exists { - if encodedStr, ok := genericCreatedEncoded.(string); ok { - if decodedBytes, err := base64.StdEncoding.DecodeString(encodedStr); err == nil { - currentState.GenericVMClassCreated = string(decodedBytes) == "true" - } - } + var moduleStateSecret corev1.Secret + if err := moduleStateSecrets[0].UnmarshalTo(&moduleStateSecret); err == nil { + if string(moduleStateSecret.Data[genericVMClassStateKey]) == "true" { + currentState.GenericVMClassCreated = true } } } diff --git a/images/hooks/pkg/hooks/update-module-state/hook_test.go b/images/hooks/pkg/hooks/update-module-state/hook_test.go index f7c8304471..3ac26a08d9 100644 --- a/images/hooks/pkg/hooks/update-module-state/hook_test.go +++ b/images/hooks/pkg/hooks/update-module-state/hook_test.go @@ -30,6 +30,7 @@ import ( "github.com/deckhouse/module-sdk/pkg" "github.com/deckhouse/module-sdk/testing/mock" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestUpdateModuleState(t *testing.T) { @@ -89,15 +90,21 @@ var _ = Describe("Update Module State hook", func() { }) It("should update module-state secret when it exists but has wrong value", func() { - moduleStateData := map[string]interface{}{ - "generic-vmclass-created": base64.StdEncoding.EncodeToString([]byte("false")), + moduleStateSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "module-state", + Namespace: settings.ModuleNamespace, + }, + Data: map[string][]byte{ + "generic-vmclass-created": []byte("false"), + }, } snapshots.GetMock.When(moduleStateSecretSnapshot).Then([]pkg.Snapshot{ mock.NewSnapshotMock(GinkgoT()).UnmarshalToMock.Set(func(v any) error { - data, ok := v.(*map[string]interface{}) + secret, ok := v.(*corev1.Secret) Expect(ok).To(BeTrue()) - *data = moduleStateData + *secret = *moduleStateSecret return nil }), }) @@ -122,15 +129,21 @@ var _ = Describe("Update Module State hook", func() { }) It("should update module-state secret even when it has correct value", func() { - moduleStateData := map[string]interface{}{ - "generic-vmclass-created": base64.StdEncoding.EncodeToString([]byte("true")), + moduleStateSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "module-state", + Namespace: settings.ModuleNamespace, + }, + Data: map[string][]byte{ + "generic-vmclass-created": []byte("true"), + }, } snapshots.GetMock.When(moduleStateSecretSnapshot).Then([]pkg.Snapshot{ mock.NewSnapshotMock(GinkgoT()).UnmarshalToMock.Set(func(v any) error { - data, ok := v.(*map[string]interface{}) + secret, ok := v.(*corev1.Secret) Expect(ok).To(BeTrue()) - *data = moduleStateData + *secret = *moduleStateSecret return nil }), }) @@ -181,15 +194,21 @@ var _ = Describe("Update Module State hook", func() { }) It("should update module-state secret and keep historical record when vmclass doesn't exist but module-state indicates it was created", func() { - moduleStateData := map[string]interface{}{ - "generic-vmclass-created": base64.StdEncoding.EncodeToString([]byte("true")), + moduleStateSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "module-state", + Namespace: settings.ModuleNamespace, + }, + Data: map[string][]byte{ + "generic-vmclass-created": []byte("true"), + }, } snapshots.GetMock.When(moduleStateSecretSnapshot).Then([]pkg.Snapshot{ mock.NewSnapshotMock(GinkgoT()).UnmarshalToMock.Set(func(v any) error { - data, ok := v.(*map[string]interface{}) + secret, ok := v.(*corev1.Secret) Expect(ok).To(BeTrue()) - *data = moduleStateData + *secret = *moduleStateSecret return nil }), }) @@ -215,15 +234,21 @@ var _ = Describe("Update Module State hook", func() { }) It("should update module-state secret when vmclass doesn't exist and secret contains false", func() { - moduleStateData := map[string]interface{}{ - "generic-vmclass-created": base64.StdEncoding.EncodeToString([]byte("false")), + moduleStateSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "module-state", + Namespace: settings.ModuleNamespace, + }, + Data: map[string][]byte{ + "generic-vmclass-created": []byte("false"), + }, } snapshots.GetMock.When(moduleStateSecretSnapshot).Then([]pkg.Snapshot{ mock.NewSnapshotMock(GinkgoT()).UnmarshalToMock.Set(func(v any) error { - data, ok := v.(*map[string]interface{}) + secret, ok := v.(*corev1.Secret) Expect(ok).To(BeTrue()) - *data = moduleStateData + *secret = *moduleStateSecret return nil }), }) @@ -252,8 +277,14 @@ var _ = Describe("Update Module State hook", func() { Context("state transition logic", func() { It("should preserve historical true value even when vmclass is deleted and recreated", func() { // First, simulate that VMClass was created and state recorded as true - moduleStateData := map[string]interface{}{ - "generic-vmclass-created": base64.StdEncoding.EncodeToString([]byte("true")), + moduleStateSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "module-state", + Namespace: settings.ModuleNamespace, + }, + Data: map[string][]byte{ + "generic-vmclass-created": []byte("true"), + }, } // VMClass doesn't exist now (was deleted) @@ -261,9 +292,9 @@ var _ = Describe("Update Module State hook", func() { snapshots.GetMock.When(moduleStateSecretSnapshot).Then([]pkg.Snapshot{ mock.NewSnapshotMock(GinkgoT()).UnmarshalToMock.Set(func(v any) error { - data, ok := v.(*map[string]interface{}) + secret, ok := v.(*corev1.Secret) Expect(ok).To(BeTrue()) - *data = moduleStateData + *secret = *moduleStateSecret return nil }), }) From c93cb4d80d38693b30ab9386c4a40e4d4249a0ab Mon Sep 17 00:00:00 2001 From: Pavel Tishkov Date: Fri, 31 Oct 2025 22:01:02 +0300 Subject: [PATCH 17/17] chore(module): generic-vmclass-was-ever-created --- .../pkg/hooks/create-generic-vmclass/hook.go | 4 +-- .../hooks/create-generic-vmclass/hook_test.go | 10 +++---- .../pkg/hooks/update-module-state/hook.go | 4 +-- .../hooks/update-module-state/hook_test.go | 30 +++++++++---------- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/images/hooks/pkg/hooks/create-generic-vmclass/hook.go b/images/hooks/pkg/hooks/create-generic-vmclass/hook.go index 2cc9a94c7d..cf81f2388c 100644 --- a/images/hooks/pkg/hooks/create-generic-vmclass/hook.go +++ b/images/hooks/pkg/hooks/create-generic-vmclass/hook.go @@ -92,14 +92,14 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { return nil } - // if module-state secret exists and contains generic-vmclass-created=true, nothing to do + // if module-state secret exists and contains generic-vmclass-was-ever-created=true, nothing to do if len(moduleStateSecrets) > 0 { var moduleStateSecret corev1.Secret if err := moduleStateSecrets[0].UnmarshalTo(&moduleStateSecret); err != nil { return err } - if string(moduleStateSecret.Data["generic-vmclass-created"]) == "true" { + if string(moduleStateSecret.Data["generic-vmclass-was-ever-created"]) == "true" { return nil } } diff --git a/images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go b/images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go index b8dcc271c0..2178ab99f1 100644 --- a/images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go +++ b/images/hooks/pkg/hooks/create-generic-vmclass/hook_test.go @@ -60,7 +60,7 @@ var _ = Describe("Create Generic VMClass hook", func() { patchCollector = nil }) - Context("when module-state secret exists with generic-vmclass-created=true", func() { + Context("when module-state secret exists with generic-vmclass-was-ever-created=true", func() { BeforeEach(func() { moduleStateSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -68,7 +68,7 @@ var _ = Describe("Create Generic VMClass hook", func() { Namespace: "d8-virtualization", }, Data: map[string][]byte{ - "generic-vmclass-created": []byte("true"), + "generic-vmclass-was-ever-created": []byte("true"), }, } @@ -117,7 +117,7 @@ var _ = Describe("Create Generic VMClass hook", func() { }) - Context("when module-state secret exists but doesn't contain generic-vmclass-created", func() { + Context("when module-state secret exists but doesn't contain generic-vmclass-was-ever-created", func() { BeforeEach(func() { moduleStateSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -158,7 +158,7 @@ var _ = Describe("Create Generic VMClass hook", func() { }) - Context("when module-state secret exists with generic-vmclass-created=false", func() { + Context("when module-state secret exists with generic-vmclass-was-ever-created=false", func() { BeforeEach(func() { moduleStateSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -166,7 +166,7 @@ var _ = Describe("Create Generic VMClass hook", func() { Namespace: "d8-virtualization", }, Data: map[string][]byte{ - "generic-vmclass-created": []byte("false"), + "generic-vmclass-was-ever-created": []byte("false"), }, } diff --git a/images/hooks/pkg/hooks/update-module-state/hook.go b/images/hooks/pkg/hooks/update-module-state/hook.go index 82d1000acc..2de434d2db 100644 --- a/images/hooks/pkg/hooks/update-module-state/hook.go +++ b/images/hooks/pkg/hooks/update-module-state/hook.go @@ -44,7 +44,7 @@ const ( apiVersion = core.GroupName + "/" + v1alpha2.Version // State fields configuration - genericVMClassStateKey = "generic-vmclass-created" + genericVMClassStateKey = "generic-vmclass-was-ever-created" ) var _ = registry.RegisterFunc(config, Reconcile) @@ -125,7 +125,7 @@ func Reconcile(_ context.Context, input *pkg.HookInput) error { } } - // Update state: generic-vmclass-created can only transition from false to true + // Update state: generic-vmclass-was-ever-created can only transition from false to true newState := ModuleState{ GenericVMClassCreated: currentState.GenericVMClassCreated || vmClassExists, } diff --git a/images/hooks/pkg/hooks/update-module-state/hook_test.go b/images/hooks/pkg/hooks/update-module-state/hook_test.go index 3ac26a08d9..23518b192c 100644 --- a/images/hooks/pkg/hooks/update-module-state/hook_test.go +++ b/images/hooks/pkg/hooks/update-module-state/hook_test.go @@ -77,9 +77,9 @@ var _ = Describe("Update Module State hook", func() { Expect(ok).To(BeTrue()) Expect(secret.Name).To(Equal("module-state")) Expect(secret.Namespace).To(Equal(settings.ModuleNamespace)) - Expect(secret.Data).To(HaveKey("generic-vmclass-created")) + Expect(secret.Data).To(HaveKey("generic-vmclass-was-ever-created")) - Expect(string(secret.Data["generic-vmclass-created"])).To(Equal("true")) + Expect(string(secret.Data["generic-vmclass-was-ever-created"])).To(Equal("true")) }) patchCollector.PatchWithMergeMock.Optional() @@ -96,7 +96,7 @@ var _ = Describe("Update Module State hook", func() { Namespace: settings.ModuleNamespace, }, Data: map[string][]byte{ - "generic-vmclass-created": []byte("false"), + "generic-vmclass-was-ever-created": []byte("false"), }, } @@ -116,9 +116,9 @@ var _ = Describe("Update Module State hook", func() { data, ok := patchData["data"].(map[string]string) Expect(ok).To(BeTrue()) - Expect(data).To(HaveKey("generic-vmclass-created")) + Expect(data).To(HaveKey("generic-vmclass-was-ever-created")) - Expect(data["generic-vmclass-created"]).To(Equal(base64.StdEncoding.EncodeToString([]byte("true")))) + Expect(data["generic-vmclass-was-ever-created"]).To(Equal(base64.StdEncoding.EncodeToString([]byte("true")))) }) patchCollector.CreateMock.Optional() @@ -135,7 +135,7 @@ var _ = Describe("Update Module State hook", func() { Namespace: settings.ModuleNamespace, }, Data: map[string][]byte{ - "generic-vmclass-created": []byte("true"), + "generic-vmclass-was-ever-created": []byte("true"), }, } @@ -155,9 +155,9 @@ var _ = Describe("Update Module State hook", func() { data, ok := patchData["data"].(map[string]string) Expect(ok).To(BeTrue()) - Expect(data).To(HaveKey("generic-vmclass-created")) + Expect(data).To(HaveKey("generic-vmclass-was-ever-created")) - Expect(data["generic-vmclass-created"]).To(Equal(base64.StdEncoding.EncodeToString([]byte("true")))) + Expect(data["generic-vmclass-was-ever-created"]).To(Equal(base64.StdEncoding.EncodeToString([]byte("true")))) }) patchCollector.CreateMock.Optional() @@ -181,9 +181,9 @@ var _ = Describe("Update Module State hook", func() { Expect(ok).To(BeTrue()) Expect(secret.Name).To(Equal("module-state")) Expect(secret.Namespace).To(Equal(settings.ModuleNamespace)) - Expect(secret.Data).To(HaveKey("generic-vmclass-created")) + Expect(secret.Data).To(HaveKey("generic-vmclass-was-ever-created")) - Expect(string(secret.Data["generic-vmclass-created"])).To(Equal("false")) + Expect(string(secret.Data["generic-vmclass-was-ever-created"])).To(Equal("false")) }) patchCollector.PatchWithMergeMock.Optional() @@ -200,7 +200,7 @@ var _ = Describe("Update Module State hook", func() { Namespace: settings.ModuleNamespace, }, Data: map[string][]byte{ - "generic-vmclass-created": []byte("true"), + "generic-vmclass-was-ever-created": []byte("true"), }, } @@ -240,7 +240,7 @@ var _ = Describe("Update Module State hook", func() { Namespace: settings.ModuleNamespace, }, Data: map[string][]byte{ - "generic-vmclass-created": []byte("false"), + "generic-vmclass-was-ever-created": []byte("false"), }, } @@ -260,10 +260,10 @@ var _ = Describe("Update Module State hook", func() { data, ok := patchData["data"].(map[string]string) Expect(ok).To(BeTrue()) - Expect(data).To(HaveKey("generic-vmclass-created")) + Expect(data).To(HaveKey("generic-vmclass-was-ever-created")) // Should remain false since VMClass doesn't exist - Expect(data["generic-vmclass-created"]).To(Equal(base64.StdEncoding.EncodeToString([]byte("false")))) + Expect(data["generic-vmclass-was-ever-created"]).To(Equal(base64.StdEncoding.EncodeToString([]byte("false")))) }) patchCollector.CreateMock.Optional() @@ -283,7 +283,7 @@ var _ = Describe("Update Module State hook", func() { Namespace: settings.ModuleNamespace, }, Data: map[string][]byte{ - "generic-vmclass-created": []byte("true"), + "generic-vmclass-was-ever-created": []byte("true"), }, }