diff --git a/.helmignore b/.helmignore index 4deeb35ea3..21b5614f21 100644 --- a/.helmignore +++ b/.helmignore @@ -7,6 +7,5 @@ lib Makefile openapi *.md -release.yaml werf*.yaml NOTES.txt diff --git a/api/core/v1alpha2/virtual_machine.go b/api/core/v1alpha2/virtual_machine.go index 32586aa0e0..15ce429803 100644 --- a/api/core/v1alpha2/virtual_machine.go +++ b/api/core/v1alpha2/virtual_machine.go @@ -275,6 +275,8 @@ type VirtualMachineStatus struct { // List of virtual machine pods. VirtualMachinePods []VirtualMachinePod `json:"virtualMachinePods,omitempty"` Resources ResourcesStatus `json:"resources,omitempty"` + // Firmware version. + FirmwareVersion string `json:"firmwareVersion,omitempty"` } type VirtualMachineStats struct { diff --git a/api/core/v1alpha2/vmcondition/condition.go b/api/core/v1alpha2/vmcondition/condition.go index 089ad58bfe..94fee740ee 100644 --- a/api/core/v1alpha2/vmcondition/condition.go +++ b/api/core/v1alpha2/vmcondition/condition.go @@ -36,9 +36,10 @@ const ( TypeConfigurationApplied Type = "ConfigurationApplied" TypeAwaitingRestartToApplyConfiguration Type = "AwaitingRestartToApplyConfiguration" // TypeFilesystemFrozen indicates whether the filesystem is currently frozen, a necessary condition for creating a snapshot. - TypeFilesystemFrozen Type = "FilesystemFrozen" - TypeSizingPolicyMatched Type = "SizingPolicyMatched" - TypeSnapshotting Type = "Snapshotting" + TypeFilesystemFrozen Type = "FilesystemFrozen" + TypeSizingPolicyMatched Type = "SizingPolicyMatched" + TypeSnapshotting Type = "Snapshotting" + TypeFirmwareUpdateRequired Type = "FirmwareUpdateRequired" ) type Reason string @@ -104,8 +105,10 @@ const ( // ReasonBlockDeviceLimitExceeded indicates that the limit for attaching block devices has been exceeded ReasonBlockDeviceLimitExceeded Reason = "BlockDeviceLimitExceeded" - ReasonPodTerminatingReason Reason = "PodTerminating" - ReasonPodNotExistsReason Reason = "PodNotExists" - ReasonPodConditionMissingReason Reason = "PodConditionMissing" - ReasonGuestNotRunningReason Reason = "GuestNotRunning" + ReasonPodTerminating Reason = "PodTerminating" + ReasonPodNotExists Reason = "PodNotExists" + ReasonPodConditionMissing Reason = "PodConditionMissing" + ReasonGuestNotRunning Reason = "GuestNotRunning" + + ReasonFirmwareUpdateRequired Reason = "FirmwareUpdateRequired" ) diff --git a/api/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go b/api/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go index 57aa75a31a..0787475472 100644 --- a/api/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go +++ b/api/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go @@ -5184,6 +5184,13 @@ func schema_virtualization_api_core_v1alpha2_VirtualMachineStatus(ref common.Ref Ref: ref("github.com/deckhouse/virtualization/api/core/v1alpha2.ResourcesStatus"), }, }, + "firmwareVersion": { + SchemaProps: spec.SchemaProps{ + Description: "Firmware version.", + Type: []string{"string"}, + Format: "", + }, + }, }, Required: []string{"phase", "nodeName", "virtualMachineIPAddressName", "ipAddress"}, }, diff --git a/crds/doc-ru-virtualmachines.yaml b/crds/doc-ru-virtualmachines.yaml index 2a2191752c..f74af6f9c5 100644 --- a/crds/doc-ru-virtualmachines.yaml +++ b/crds/doc-ru-virtualmachines.yaml @@ -696,3 +696,5 @@ spec: description: Накладные расходы на память во время выполнения. size: description: Текущий размер памяти виртуальной машины. + firmwareVersion: + description: Версия firmware. diff --git a/crds/virtualmachines.yaml b/crds/virtualmachines.yaml index 3017200f79..aadf587276 100644 --- a/crds/virtualmachines.yaml +++ b/crds/virtualmachines.yaml @@ -1241,6 +1241,9 @@ spec: - size type: object type: object + firmwareVersion: + type: string + description: Firmware version. additionalPrinterColumns: - description: Virtual machine phase. jsonPath: .status.phase diff --git a/cv/version_map.yml b/cv/version_map.yml index 7319b6102e..bac8169b2c 100644 --- a/cv/version_map.yml +++ b/cv/version_map.yml @@ -1,4 +1,13 @@ firmware: + # Don't touch! Generating in CI. + version: main + ## Components qemu: 9.2.0 libvirt: 10.9.0 edk2: stable202411 + +module: + # Don't touch! Generating in CI. + version: main + # Should be updated manual. + firmwareMinSupportedVersion: "v0.17.0" diff --git a/images/virtualization-artifact/cmd/virtualization-controller/main.go b/images/virtualization-artifact/cmd/virtualization-controller/main.go index 2f018900e2..38f27dd0ac 100644 --- a/images/virtualization-artifact/cmd/virtualization-controller/main.go +++ b/images/virtualization-artifact/cmd/virtualization-controller/main.go @@ -73,6 +73,8 @@ const ( pprofBindAddrEnv = "PPROF_BIND_ADDRESS" virtualMachineCIDRsEnv = "VIRTUAL_MACHINE_CIDRS" virtualMachineIPLeasesRetentionDurationEnv = "VIRTUAL_MACHINE_IP_LEASES_RETENTION_DURATION" + + FirmwareImageEnv = "FIRMWARE_IMAGE" ) func main() { @@ -110,6 +112,9 @@ func main() { var metricsBindAddr string flag.StringVar(&metricsBindAddr, "metrics-bind-address", getEnv(metricsBindAddrEnv, ":8080"), "metric bind address") + var firmwareImage string + flag.StringVar(&firmwareImage, "firmware-image", os.Getenv(FirmwareImageEnv), "Firmware image") + flag.Parse() log := logger.NewLogger(logLevel, logOutput, logDebugVerbosity) @@ -117,6 +122,11 @@ func main() { printVersion(log) + if firmwareImage == "" { + log.Error("firmware image is required") + os.Exit(1) + } + controllerNamespace, err := appconfig.GetRequiredEnvVar(podNamespaceEnv) if err != nil { log.Error(err.Error()) @@ -261,7 +271,7 @@ func main() { } vmLogger := logger.NewControllerLogger(vm.ControllerName, logLevel, logOutput, logDebugVerbosity, logDebugControllerList) - if err = vm.SetupController(ctx, mgr, vmLogger, dvcrSettings); err != nil { + if err = vm.SetupController(ctx, mgr, vmLogger, dvcrSettings, firmwareImage); err != nil { log.Error(err.Error()) os.Exit(1) } diff --git a/images/virtualization-artifact/go.mod b/images/virtualization-artifact/go.mod index c2375d99e7..83c5327e03 100644 --- a/images/virtualization-artifact/go.mod +++ b/images/virtualization-artifact/go.mod @@ -13,6 +13,7 @@ require ( github.com/onsi/gomega v1.31.0 github.com/prometheus/client_golang v1.18.0 github.com/robfig/cron/v3 v3.0.1 + github.com/rogpeppe/go-internal v1.10.0 github.com/spf13/cobra v1.7.0 github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.26.0 @@ -85,7 +86,7 @@ require ( github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/openshift/custom-resource-status v1.1.2 // indirect - github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/errors v0.9.1 github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.5.0 github.com/prometheus/common v0.45.0 // indirect @@ -108,7 +109,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.31.0 // indirect golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect - golang.org/x/mod v0.17.0 // indirect + golang.org/x/mod v0.17.0 golang.org/x/net v0.25.0 // indirect golang.org/x/oauth2 v0.12.0 // indirect golang.org/x/sync v0.10.0 // indirect diff --git a/images/virtualization-artifact/pkg/common/testutil/testutil.go b/images/virtualization-artifact/pkg/common/testutil/testutil.go index b9ebf41843..183b821259 100644 --- a/images/virtualization-artifact/pkg/common/testutil/testutil.go +++ b/images/virtualization-artifact/pkg/common/testutil/testutil.go @@ -17,6 +17,10 @@ limitations under the License. package testutil import ( + "context" + "log/slog" + + "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" apiruntime "k8s.io/apimachinery/pkg/runtime" virtv1 "kubevirt.io/api/core/v1" @@ -41,9 +45,17 @@ func NewFakeClientWithObjects(objs ...client.Object) (client.WithWatch, error) { return nil, err } } - return fake.NewClientBuilder().WithScheme(scheme).WithObjects(objs...).Build(), nil + return fake.NewClientBuilder().WithScheme(scheme).WithObjects(objs...).WithStatusSubresource(objs...).Build(), nil } func NewNoOpLogger() *log.Logger { return log.NewNop() } + +func ToContext(ctx context.Context, log *log.Logger) context.Context { + return logr.NewContextWithSlogLogger(ctx, slog.New(log.Handler())) +} + +func ContextBackgroundWithNoOpLogger() context.Context { + return ToContext(context.Background(), NewNoOpLogger()) +} diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/block_devices_test.go b/images/virtualization-artifact/pkg/controller/vm/internal/block_devices_test.go index 82a7007aad..1fcd179866 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/block_devices_test.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/block_devices_test.go @@ -20,7 +20,6 @@ import ( "context" "fmt" "log/slog" - "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -43,11 +42,6 @@ import ( "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" ) -func TestBlockDeviceHandler(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "BlockDeviceHandler Suite") -} - var _ = Describe("func areVirtualDisksAllowedToUse", func() { var h *BlockDeviceHandler var vdFoo *virtv2.VirtualDisk diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/firmware.go b/images/virtualization-artifact/pkg/controller/vm/internal/firmware.go new file mode 100644 index 0000000000..e0e8af34a0 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vm/internal/firmware.go @@ -0,0 +1,119 @@ +/* +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 internal + +import ( + "context" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/deckhouse/virtualization-controller/pkg/controller/conditions" + "github.com/deckhouse/virtualization-controller/pkg/controller/vm/internal/state" + "github.com/deckhouse/virtualization-controller/pkg/version" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" +) + +const firmwareHandler = "FirmwareHandler" + +func NewFirmwareHandler(firmwareImage string) *FirmwareHandler { + return &FirmwareHandler{ + firmwareVersion: version.GetFirmwareVersion(), + firmwareMinSupportedVersion: version.GetFirmwareMinSupportedVersion(), + firmwareImage: firmwareImage, + } +} + +type FirmwareHandler struct { + firmwareVersion version.Version + firmwareMinSupportedVersion version.Version + firmwareImage string +} + +func (f FirmwareHandler) Handle(ctx context.Context, s state.VirtualMachineState) (reconcile.Result, error) { + if s.VirtualMachine().IsEmpty() { + return reconcile.Result{}, nil + } + changed := s.VirtualMachine().Changed() + + kvvmi, err := s.KVVMI(ctx) + if err != nil { + return reconcile.Result{}, err + } + + if kvvmi == nil || kvvmi.Status.LauncherContainerImageVersion == f.firmwareImage { + // If kvvmi does not exist, update the firmware version, + // as any newly created kvvmi will use the currently available firmware version. + changed.Status.FirmwareVersion = f.firmwareVersion.String() + f.removeCondition(changed) + return reconcile.Result{}, nil + } + if f.needUpdate(changed.Status.FirmwareVersion, kvvmi.Status.LauncherContainerImageVersion) { + f.addCondition(changed) + return reconcile.Result{}, nil + } + + f.removeCondition(changed) + + return reconcile.Result{}, nil +} + +func (f FirmwareHandler) Name() string { + return firmwareHandler +} + +func (f FirmwareHandler) needUpdate(currentVersion, firmwareImage string) bool { + if currentVersion == "" { + return true + } + currVersion := version.Version(currentVersion) + + if !currVersion.IsValid() { + return true + } + + if f.firmwareVersion.Compare(currVersion) == 0 { + // Update is required if the version is 'main', but the virt-launcher images are different + if f.firmwareVersion.IsMain() { + return f.firmwareImage != firmwareImage + } + return false + } + + // Update is required if the current version is less than the minimum supported version. + if currVersion.Compare(f.firmwareMinSupportedVersion) == -1 { + return true + } + + // Update is required if the current version is greater than the firmware version + return currVersion.Compare(f.firmwareVersion) == 1 +} + +func (f FirmwareHandler) addCondition(changed *virtv2.VirtualMachine) { + conditions.SetCondition(conditions.NewConditionBuilder(vmcondition.TypeFirmwareUpdateRequired). + Generation(changed.GetGeneration()). + Status(metav1.ConditionTrue). + Reason(vmcondition.ReasonFirmwareUpdateRequired). + Message("The VM firmware is outdated and not recommended for use by the current version of the module, please migrate or reboot the VM to upgrade to the new firmware version."), + &changed.Status.Conditions, + ) +} + +func (f FirmwareHandler) removeCondition(changed *virtv2.VirtualMachine) { + conditions.RemoveCondition(vmcondition.TypeFirmwareUpdateRequired, &changed.Status.Conditions) +} diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/firmware_test.go b/images/virtualization-artifact/pkg/controller/vm/internal/firmware_test.go new file mode 100644 index 0000000000..ecf07693b5 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vm/internal/firmware_test.go @@ -0,0 +1,116 @@ +/* +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 internal + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + virtv1 "kubevirt.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/deckhouse/virtualization-controller/pkg/common/testutil" + "github.com/deckhouse/virtualization-controller/pkg/controller/reconciler" + "github.com/deckhouse/virtualization-controller/pkg/controller/vm/internal/state" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition" +) + +var _ = Describe("FirmwareHandler", func() { + const ( + vmName = "vm" + vmNamespace = "default" + firmwareImage = "image" + ) + + var ( + ctx = testutil.ContextBackgroundWithNoOpLogger() + fakeClient client.WithWatch + resource *reconciler.Resource[*virtv2.VirtualMachine, virtv2.VirtualMachineStatus] + vmState state.VirtualMachineState + ) + + AfterEach(func() { + fakeClient = nil + resource = nil + vmState = nil + }) + + reconcile := func() { + h := NewFirmwareHandler(firmwareImage) + h.firmwareMinSupportedVersion = "v0.70.0" + h.firmwareVersion = "v0.99.1" + + _, err := h.Handle(ctx, vmState) + Expect(err).NotTo(HaveOccurred()) + err = resource.Update(ctx) + Expect(err).NotTo(HaveOccurred()) + } + + newVm := func(version string) *virtv2.VirtualMachine { + vm := newEmptyVirtualMachine(vmName, vmNamespace) + vm.Status.FirmwareVersion = version + return vm + } + + newVmWithCond := func(version string) *virtv2.VirtualMachine { + vm := newVm(version) + vm.Status.Conditions = append(vm.Status.Conditions, metav1.Condition{ + Type: vmcondition.TypeFirmwareUpdateRequired.String(), + Status: metav1.ConditionTrue, + }) + return vm + } + + newKVVMI := func(image string) *virtv1.VirtualMachineInstance { + kvvmi := newEmptyKVVMI(vmName, vmNamespace) + kvvmi.Status.LauncherContainerImageVersion = image + return kvvmi + } + + DescribeTable("Check condition FirmwareNeedUpdate", func(vm *virtv2.VirtualMachine, kvvmi *virtv1.VirtualMachineInstance, condExist, firmwareUpdated bool) { + oldVersion := vm.Status.FirmwareVersion + fakeClient, resource, vmState = setupEnvironment(vm, kvvmi) + reconcile() + + newVM := &virtv2.VirtualMachine{} + err := fakeClient.Get(ctx, client.ObjectKey{Name: vmName, Namespace: vmNamespace}, newVM) + Expect(err).NotTo(HaveOccurred()) + + if condExist { + Expect(newVM.Status.Conditions).To(HaveLen(1)) + Expect(newVM.Status.Conditions[0].Type).To(Equal(vmcondition.TypeFirmwareUpdateRequired.String())) + Expect(newVM.Status.Conditions[0].Status).To(Equal(metav1.ConditionTrue)) + } + + newVersion := newVM.Status.FirmwareVersion + if firmwareUpdated { + Expect(oldVersion).NotTo(Equal(newVersion)) + } else { + Expect(oldVersion).To(Equal(newVersion)) + } + }, + Entry("Condition should be removed because the firmware version is supported", + newVmWithCond("v0.80.1"), newKVVMI(""), false, false), + Entry("Condition should be removed, and the firmware should be updated because the firmware version is not supported, but the VM is stopped", + newVmWithCond("v0.10.0"), nil, false, true), + Entry("Condition should be added because the firmware version is not supported (older) and the VM is running", + newVm("v0.10.0"), newKVVMI(""), true, false), + Entry("Condition should be added because the firmware version is not supported (newer) and the VM is running", + newVm("v1.10.0"), newKVVMI(""), true, false), + ) +}) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/suite_test.go b/images/virtualization-artifact/pkg/controller/vm/internal/suite_test.go new file mode 100644 index 0000000000..bb460e9b42 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vm/internal/suite_test.go @@ -0,0 +1,99 @@ +/* +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 internal + +import ( + "context" + "reflect" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + virtv1 "kubevirt.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/deckhouse/virtualization-controller/pkg/common/testutil" + "github.com/deckhouse/virtualization-controller/pkg/controller/reconciler" + "github.com/deckhouse/virtualization-controller/pkg/controller/vm/internal/state" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +func TestVirtualMachine(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "VirtualMachine Handlers Suite") +} + +func setupEnvironment(vm *virtv2.VirtualMachine, objs ...client.Object) (client.WithWatch, *reconciler.Resource[*virtv2.VirtualMachine, virtv2.VirtualMachineStatus], state.VirtualMachineState) { + GinkgoHelper() + Expect(vm).ToNot(BeNil()) + allObjects := []client.Object{vm} + for _, obj := range objs { + if reflect.ValueOf(obj).IsNil() { + continue + } + allObjects = append(allObjects, obj) + } + + fakeClient, err := testutil.NewFakeClientWithObjects(allObjects...) + Expect(err).NotTo(HaveOccurred()) + + key := types.NamespacedName{ + Name: vm.GetName(), + Namespace: vm.GetNamespace(), + } + resource := reconciler.NewResource(key, fakeClient, + func() *virtv2.VirtualMachine { + return &virtv2.VirtualMachine{} + }, + func(obj *virtv2.VirtualMachine) virtv2.VirtualMachineStatus { + return obj.Status + }) + err = resource.Fetch(context.Background()) + Expect(err).NotTo(HaveOccurred()) + + vmState := state.New(fakeClient, resource) + + return fakeClient, resource, vmState +} + +func newEmptyVirtualMachine(name, namespace string) *virtv2.VirtualMachine { + return &virtv2.VirtualMachine{ + TypeMeta: metav1.TypeMeta{ + APIVersion: virtv2.SchemeGroupVersion.String(), + Kind: virtv2.VirtualMachineKind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + } +} + +func newEmptyKVVMI(name, namespace string) *virtv1.VirtualMachineInstance { + return &virtv1.VirtualMachineInstance{ + TypeMeta: metav1.TypeMeta{ + APIVersion: virtv1.GroupVersion.String(), + Kind: "VirtualMachineInstance", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + } +} diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state_test.go b/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state_test.go index 09f949bc99..5f7a238ca4 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state_test.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/sync_power_state_test.go @@ -104,7 +104,7 @@ var _ = Describe("Test power actions with VMs", func() { }, } - vm, kvvm, kvvmi, vmPod = createObjects(namespacedVirtualMachine) + vm, kvvm, kvvmi, vmPod = createObjectsForPowerstateTest(namespacedVirtualMachine) }) It("should handle start", func() { @@ -171,7 +171,7 @@ var _ = Describe("Test action getters for different run policy", func() { Name: "ns", } - vm, kvvm, kvvmi, vmPod = createObjects(namespacedVirtualMachine) + vm, kvvm, kvvmi, vmPod = createObjectsForPowerstateTest(namespacedVirtualMachine) fakeClient = fake.NewClientBuilder(). WithScheme(scheme). WithObjects(vm, kvvm, kvvmi, vmPod). @@ -414,7 +414,7 @@ func setupScheme() *runtime.Scheme { return scheme } -func createObjects(namespacedVirtualMachine types.NamespacedName) (*virtv2.VirtualMachine, *virtv1.VirtualMachine, *virtv1.VirtualMachineInstance, *corev1.Pod) { +func createObjectsForPowerstateTest(namespacedVirtualMachine types.NamespacedName) (*virtv2.VirtualMachine, *virtv1.VirtualMachine, *virtv1.VirtualMachineInstance, *corev1.Pod) { vm := &virtv2.VirtualMachine{ ObjectMeta: metav1.ObjectMeta{ Name: namespacedVirtualMachine.Name, diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/util.go b/images/virtualization-artifact/pkg/controller/vm/internal/util.go index 6af2c97f20..b6eeaf47dd 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/util.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/util.go @@ -204,13 +204,13 @@ func getKVMIReadyReason(kvmiReason string) conditions.Stringer { var mapReasons = map[string]vmcondition.Reason{ // PodTerminatingReason indicates on the Ready condition on the VMI if the underlying pod is terminating - virtv1.PodTerminatingReason: vmcondition.ReasonPodTerminatingReason, + virtv1.PodTerminatingReason: vmcondition.ReasonPodTerminating, // PodNotExistsReason indicates on the Ready condition on the VMI if the underlying pod does not exist - virtv1.PodNotExistsReason: vmcondition.ReasonPodNotExistsReason, + virtv1.PodNotExistsReason: vmcondition.ReasonPodNotExists, // PodConditionMissingReason indicates on the Ready condition on the VMI if the underlying pod does not report a Ready condition - virtv1.PodConditionMissingReason: vmcondition.ReasonPodConditionMissingReason, + virtv1.PodConditionMissingReason: vmcondition.ReasonPodConditionMissing, // GuestNotRunningReason indicates on the Ready condition on the VMI if the underlying guest VM is not running - virtv1.GuestNotRunningReason: vmcondition.ReasonGuestNotRunningReason, + virtv1.GuestNotRunningReason: vmcondition.ReasonGuestNotRunning, } func isPodStartedError(vm *virtv1.VirtualMachine) bool { diff --git a/images/virtualization-artifact/pkg/controller/vm/vm_controller.go b/images/virtualization-artifact/pkg/controller/vm/vm_controller.go index 5efc25f46e..8437c95c51 100644 --- a/images/virtualization-artifact/pkg/controller/vm/vm_controller.go +++ b/images/virtualization-artifact/pkg/controller/vm/vm_controller.go @@ -46,6 +46,7 @@ func SetupController( mgr manager.Manager, log *log.Logger, dvcrSettings *dvcr.Settings, + firmwareImage string, ) error { recorder := eventrecord.NewEventRecorderLogger(mgr, ControllerName) mgrCache := mgr.GetCache() @@ -67,6 +68,7 @@ func SetupController( internal.NewSyncMetadataHandler(client), internal.NewLifeCycleHandler(client, recorder), internal.NewStatisticHandler(client), + internal.NewFirmwareHandler(firmwareImage), } r := NewReconciler(client, handlers...) diff --git a/images/virtualization-artifact/pkg/monitoring/metrics/virtualmachine/data_metric.go b/images/virtualization-artifact/pkg/monitoring/metrics/virtualmachine/data_metric.go index fe71aa4f94..d40c0dfadd 100644 --- a/images/virtualization-artifact/pkg/monitoring/metrics/virtualmachine/data_metric.go +++ b/images/virtualization-artifact/pkg/monitoring/metrics/virtualmachine/data_metric.go @@ -48,6 +48,7 @@ type dataMetric struct { Pods []virtv2.VirtualMachinePod Labels map[string]string Annotations map[string]string + currentFirmwareVersion string } // DO NOT mutate VirtualMachine! @@ -105,6 +106,7 @@ func newDataMetric(vm *virtv2.VirtualMachine) *dataMetric { Annotations: promutil.WrapPrometheusLabels(vm.GetAnnotations(), "annotation", func(key, _ string) bool { return strings.HasPrefix(key, "kubectl.kubernetes.io") }), + currentFirmwareVersion: vm.Status.FirmwareVersion, } } diff --git a/images/virtualization-artifact/pkg/monitoring/metrics/virtualmachine/metrics.go b/images/virtualization-artifact/pkg/monitoring/metrics/virtualmachine/metrics.go index f512d6e9a7..6bba72a303 100644 --- a/images/virtualization-artifact/pkg/monitoring/metrics/virtualmachine/metrics.go +++ b/images/virtualization-artifact/pkg/monitoring/metrics/virtualmachine/metrics.go @@ -38,6 +38,7 @@ const ( MetricVirtualMachinePod = "virtualmachine_pod" MetricVirtualMachineLabels = "virtualmachine_labels" MetricVirtualMachineAnnotations = "virtualmachine_annotations" + MetricVirtualMachineInfo = "virtualmachine_info" ) var baseLabels = []string{"name", "namespace", "uid", "node"} @@ -164,4 +165,11 @@ var virtualMachineMetrics = map[string]metrics.MetricInfo{ WithBaseLabels(), nil, ), + + MetricVirtualMachineInfo: metrics.NewMetricInfo(MetricVirtualMachineInfo, + "The virtualmachine info.", + prometheus.GaugeValue, + WithBaseLabels("firmware", "desired_firmware"), + nil, + ), } diff --git a/images/virtualization-artifact/pkg/monitoring/metrics/virtualmachine/scraper.go b/images/virtualization-artifact/pkg/monitoring/metrics/virtualmachine/scraper.go index 1764460641..c1d6e4858e 100644 --- a/images/virtualization-artifact/pkg/monitoring/metrics/virtualmachine/scraper.go +++ b/images/virtualization-artifact/pkg/monitoring/metrics/virtualmachine/scraper.go @@ -24,6 +24,7 @@ import ( "github.com/deckhouse/deckhouse/pkg/log" "github.com/deckhouse/virtualization-controller/pkg/common" "github.com/deckhouse/virtualization-controller/pkg/monitoring/metrics/promutil" + "github.com/deckhouse/virtualization-controller/pkg/version" virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" ) @@ -52,6 +53,7 @@ func (s *scraper) Report(m *dataMetric) { s.updateMetricVirtualMachineLabels(m) s.updateMetricVirtualMachineAnnotations(m) s.updateMetricVirtualMachineAgentReady(m) + s.updateMetricVirtualMachineInfo(m) } func (s *scraper) updateMetricVirtualMachineStatusPhase(m *dataMetric) { @@ -159,6 +161,12 @@ func (s *scraper) updateMetricVirtualMachineAnnotations(m *dataMetric) { s.updateDynamic(MetricVirtualMachineAnnotations, 1, m, nil, m.Annotations) } +func (s *scraper) updateMetricVirtualMachineInfo(m *dataMetric) { + s.defaultUpdate(MetricVirtualMachineInfo, 1, m, m.currentFirmwareVersion, desiredFirmwareVersion.String()) +} + +var desiredFirmwareVersion = version.GetFirmwareVersion() + func (s *scraper) defaultUpdate(name string, value float64, m *dataMetric, labelValues ...string) { info := virtualMachineMetrics[name] metric, err := prometheus.NewConstMetric( diff --git a/images/virtualization-artifact/pkg/version/firmware.go b/images/virtualization-artifact/pkg/version/firmware.go new file mode 100644 index 0000000000..a9f5e08a54 --- /dev/null +++ b/images/virtualization-artifact/pkg/version/firmware.go @@ -0,0 +1,70 @@ +/* +Copyright 2024 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 version + +import ( + _ "embed" + "fmt" + + "gopkg.in/yaml.v3" +) + +var firmwareInstance firmwareConfig + +//go:embed version_map.yml +var embeddedConfig string + +type firmwareConfig struct { + Version Version + MinSupportedVersion Version +} +type cvConfig struct { + Firmware firmware `yaml:"firmware"` + Module module `yaml:"module"` +} + +type firmware struct { + Version Version `yaml:"version"` +} +type module struct { + Version Version `yaml:"version"` + FirmwareMinSupportedVersion Version `yaml:"firmwareMinSupportedVersion"` +} + +func (f firmwareConfig) Validate() error { + if !f.Version.IsValid() { + return fmt.Errorf("firmware version is invalid") + } + if !f.MinSupportedVersion.IsValid() { + return fmt.Errorf("firmware minimum supported version is invalid") + } + return nil +} + +func init() { + cvConf := cvConfig{} + if err := yaml.Unmarshal([]byte(embeddedConfig), &cvConf); err != nil { + panic("failed to load embedded component version config: " + err.Error()) + } + + firmwareInstance.Version = cvConf.Firmware.Version + firmwareInstance.MinSupportedVersion = cvConf.Module.FirmwareMinSupportedVersion + + if err := firmwareInstance.Validate(); err != nil { + panic("failed to validate embedded firmwareConf: " + err.Error()) + } +} diff --git a/images/virtualization-artifact/pkg/version/get.go b/images/virtualization-artifact/pkg/version/get.go new file mode 100644 index 0000000000..876a4c6360 --- /dev/null +++ b/images/virtualization-artifact/pkg/version/get.go @@ -0,0 +1,35 @@ +/* +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 version + +func GetEdition() string { + return edition +} + +func GetFirmwareVersion() Version { + if !firmwareInstance.Version.IsValid() { + panic("firmware version is invalid") + } + return firmwareInstance.Version +} + +func GetFirmwareMinSupportedVersion() Version { + if !firmwareInstance.MinSupportedVersion.IsValid() { + panic("firmware minimum supported version is invalid") + } + return firmwareInstance.MinSupportedVersion +} diff --git a/images/virtualization-artifact/pkg/version/version.go b/images/virtualization-artifact/pkg/version/version.go index 97b16e4cf2..07ac2b484a 100644 --- a/images/virtualization-artifact/pkg/version/version.go +++ b/images/virtualization-artifact/pkg/version/version.go @@ -1,5 +1,5 @@ /* -Copyright 2024 Flant JSC +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. @@ -16,6 +16,36 @@ limitations under the License. package version -func GetEdition() string { - return edition +import "golang.org/x/mod/semver" + +const mainVersion Version = "main" + +type Version string + +func (v Version) IsValid() bool { + return v == mainVersion || semver.IsValid(string(v)) +} + +func (v Version) IsMain() bool { + return v == mainVersion +} + +func (v Version) String() string { + return string(v) +} + +func (v Version) Compare(v2 Version) int { + vIsMain := v.IsMain() + v2IsMain := v2.IsMain() + + switch { + case vIsMain && v2IsMain: + return 0 + case vIsMain: + return 1 + case v2IsMain: + return -1 + } + + return semver.Compare(v.String(), v2.String()) } diff --git a/images/virtualization-artifact/pkg/version/version_map.yml b/images/virtualization-artifact/pkg/version/version_map.yml new file mode 100644 index 0000000000..7b6451143a --- /dev/null +++ b/images/virtualization-artifact/pkg/version/version_map.yml @@ -0,0 +1,14 @@ +# Just stub file +firmware: + # Don't touch! Generating in CI. + version: main + ## Components + qemu: 0.0.0 + libvirt: 0.0.0 + edk2: stable000 + +module: + # Don't touch! Generating in CI. + version: main + # Should be updated manual. + firmwareMinSupportedVersion: "v0.17.0" diff --git a/images/virtualization-artifact/werf.inc.yaml b/images/virtualization-artifact/werf.inc.yaml index 1f23c8f165..0cf528f7e9 100644 --- a/images/virtualization-artifact/werf.inc.yaml +++ b/images/virtualization-artifact/werf.inc.yaml @@ -19,6 +19,14 @@ git: - go.sum setup: - "**/*.go" +- add: /cv/version_map.yml + to: /usr/local/go/src/virtualization-controller/pkg/version/version_map.yml + stageDependencies: + install: + - go.mod + - go.sum + setup: + - "**/*.go" mount: - fromPath: ~/go-pkg-cache to: /go/pkg diff --git a/release.yaml b/release.yaml deleted file mode 100644 index 128c3d74f0..0000000000 --- a/release.yaml +++ /dev/null @@ -1,2 +0,0 @@ -# Module version -version: v0.0.1 diff --git a/templates/virtualization-controller/_helpers.tpl b/templates/virtualization-controller/_helpers.tpl index 548f537b86..0947766b75 100644 --- a/templates/virtualization-controller/_helpers.tpl +++ b/templates/virtualization-controller/_helpers.tpl @@ -76,4 +76,6 @@ - name: PPROF_BIND_ADDRESS value: ":8081" {{- end }} +- name: FIRMWARE_IMAGE + value: {{ include "helm_lib_module_image" (list . "virtLauncher") }} {{- end }}