Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
4476a33
add maintenance
yaroslavborbat Aug 5, 2025
b26c7be
add maintenance
yaroslavborbat Aug 5, 2025
97f5222
add maintenance
loktev-d Aug 7, 2025
957d57c
fixes
loktev-d Aug 12, 2025
312c855
add annotation for testing
loktev-d Aug 13, 2025
def0204
fixes
loktev-d Aug 13, 2025
43b0042
remove vmop
loktev-d Aug 13, 2025
bb19ec0
fixes
loktev-d Aug 13, 2025
0b0c4d8
fixes
loktev-d Aug 14, 2025
4a22538
fixes
loktev-d Aug 14, 2025
625f874
fixes
loktev-d Aug 14, 2025
fecf8b0
fixes
loktev-d Aug 14, 2025
651942a
fixes
loktev-d Aug 14, 2025
6184931
fixes
loktev-d Aug 14, 2025
13da17c
fixes
loktev-d Aug 14, 2025
c0abc7b
fixes
loktev-d Aug 14, 2025
bbc4fbd
add check for syncPowerState
loktev-d Aug 14, 2025
3cafbe2
fixes
loktev-d Aug 14, 2025
f8ccf17
fixes
loktev-d Aug 14, 2025
6f0dc0c
fixes
loktev-d Aug 14, 2025
31e11db
fixes
loktev-d Aug 14, 2025
c3b7abb
fixes
loktev-d Aug 14, 2025
6d6bcf7
fix linter errors
loktev-d Aug 14, 2025
f6ef9f3
fixes
loktev-d Aug 25, 2025
074a805
fixes
loktev-d Aug 25, 2025
b2917b6
add comments
loktev-d Aug 25, 2025
8256de6
add annotation for testing
loktev-d Aug 25, 2025
12a30fe
Merge branch 'main' into feat/vm/maitance-mode
loktev-d Aug 25, 2025
eb1de7d
fixes
loktev-d Aug 25, 2025
6ed6694
fixes
loktev-d Aug 25, 2025
ad1f841
fixes
loktev-d Aug 25, 2025
6c3d8a1
fixes
loktev-d Aug 25, 2025
013bceb
fixes
loktev-d Aug 25, 2025
888f4c6
remove annotation
loktev-d Aug 25, 2025
8eda7c5
fixes
loktev-d Aug 25, 2025
a2cd477
fixes
loktev-d Aug 25, 2025
ee6553e
Merge branch 'main' into feat/vm/maitance-mode
loktev-d Aug 25, 2025
ed14f1e
remove annotation
loktev-d Aug 25, 2025
a9993c0
fixes
loktev-d Aug 25, 2025
9bc3856
Merge branch 'main' into feat/vm/maitance-mode
loktev-d Aug 26, 2025
a0bc1aa
Merge branch 'main' into feat/vm/maitance-mode
loktev-d Aug 26, 2025
251798a
fixes
loktev-d Aug 26, 2025
8c9adc2
fixes
loktev-d Aug 26, 2025
0cf4ee3
Merge branch 'main' into feat/vm/maitance-mode
loktev-d Aug 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions api/core/v1alpha2/vmcondition/condition.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ const (
TypeNeedsEvict Type = "NeedsEvict"
// TypeNetworkReady indicates the state of additional network interfaces inside the virtual machine pod
TypeNetworkReady Type = "NetworkReady"

// TypeMaintenance indicates that the VirtualMachine is in maintenance mode.
// During this condition, the VM remains stopped and no changes are allowed.
TypeMaintenance Type = "Maintenance"
)

type Reason string
Expand Down Expand Up @@ -132,4 +136,7 @@ const (
ReasonNetworkNotReady Reason = "NetworkNotReady"
// ReasonSDNModuleDisable indicates that the SDN module is disabled, which may prevent network interfaces from becoming ready.
ReasonSDNModuleDisable Reason = "SDNModuleDisable"

// ReasonMaintenanceRestore indicates that the VirtualMachine is in maintenance mode for restore operation.
ReasonMaintenanceRestore Reason = "RestoreInProgress"
)
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import (
"github.com/deckhouse/virtualization-controller/pkg/logger"
)

var ErrStopHandlerChain = errors.New("stop handler chain execution")

type Handler[T client.Object] interface {
Handle(ctx context.Context, obj T) (reconcile.Result, error)
Name() string
Expand Down Expand Up @@ -77,12 +79,17 @@ func (r *BaseReconciler[H]) Reconcile(ctx context.Context) (reconcile.Result, er
var result reconcile.Result
var errs error

handlersLoop:
for _, h := range r.handlers {
log := logger.FromContext(ctx).With(logger.SlogHandler(reflect.TypeOf(h).Elem().Name()))

res, err := r.execute(ctx, h)
switch {
case err == nil: // OK.
case errors.Is(err, ErrStopHandlerChain):
log.Debug("Handler chain execution stopped")
result = MergeResults(result, res)
break handlersLoop
case k8serrors.IsConflict(err):
log.Debug("Conflict occurred during handler execution", logger.SlogErr(err))
result.RequeueAfter = 100 * time.Microsecond
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
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"
"fmt"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

"github.com/deckhouse/virtualization-controller/pkg/common/object"
"github.com/deckhouse/virtualization-controller/pkg/controller/conditions"
"github.com/deckhouse/virtualization-controller/pkg/controller/reconciler"
"github.com/deckhouse/virtualization-controller/pkg/controller/vm/internal/state"
"github.com/deckhouse/virtualization-controller/pkg/logger"
"github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition"
)

const nameMaintenanceHandler = "MaintenanceHandler"

type MaintenanceHandler struct {
client client.Client
}

func NewMaintenanceHandler(client client.Client) *MaintenanceHandler {
return &MaintenanceHandler{
client: client,
}
}

func (h *MaintenanceHandler) Handle(ctx context.Context, s state.VirtualMachineState) (reconcile.Result, error) {
log := logger.FromContext(ctx)

if s.VirtualMachine().IsEmpty() {
return reconcile.Result{}, nil
}
changed := s.VirtualMachine().Changed()

maintenance, _ := conditions.GetCondition(vmcondition.TypeMaintenance, changed.Status.Conditions)

if maintenance.Status == metav1.ConditionFalse {
conditions.RemoveCondition(vmcondition.TypeMaintenance, &changed.Status.Conditions)
return reconcile.Result{}, nil
}

if maintenance.Status != metav1.ConditionTrue {
return reconcile.Result{}, nil
}

log.Info("Reconcile observe a VirtualMachine in maintenance mode")

kvvmi, err := s.KVVMI(ctx)
if err != nil {
return reconcile.Result{}, fmt.Errorf("get KVVMI: %w", err)
}

// If VM is not stopped yet, wait for it to stop (SyncPowerStateHandler will handle stopping)
if kvvmi != nil {
log.Info("VM is still running, waiting for shutdown in maintenance mode")
return reconcile.Result{}, nil
}

// Hide all other conditions when in maintenance mode
changed.Status.Conditions = []metav1.Condition{maintenance}

log.Info("VM is stopped, cleaning up resources if any for maintenance mode")

kvvm, err := s.KVVM(ctx)
if err != nil {
return reconcile.Result{}, fmt.Errorf("%w: get KVVM: %w", reconciler.ErrStopHandlerChain, err)
}
if kvvm != nil {
log.Info("Deleting KVVM for maintenance mode")
err = object.CleanupObject(ctx, h.client, kvvm)
if err != nil {
return reconcile.Result{}, fmt.Errorf("%w: delete KVVM: %w", reconciler.ErrStopHandlerChain, err)
}
}

pods, err := s.Pods(ctx)
if err != nil {
return reconcile.Result{}, fmt.Errorf("%w: get pods: %w", reconciler.ErrStopHandlerChain, err)
}
if pods != nil && len(pods.Items) > 0 {
log.Info("Deleting pods for maintenance mode")
for i := range pods.Items {
err = object.CleanupObject(ctx, h.client, &pods.Items[i])
if err != nil {
return reconcile.Result{}, fmt.Errorf("%w: delete pod: %w", reconciler.ErrStopHandlerChain, err)
}
}
}

return reconcile.Result{}, reconciler.ErrStopHandlerChain
}

func (h *MaintenanceHandler) Name() string {
return nameMaintenanceHandler
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,23 +126,30 @@ func (h *SyncPowerStateHandler) syncPowerState(
shutdownInfo = s.ShutdownInfo
})

isConfigurationApplied := checkVirtualMachineConfiguration(s.VirtualMachine().Changed())
changed := s.VirtualMachine().Changed()
isConfigurationApplied := checkVirtualMachineConfiguration(changed)
maintenance, _ := conditions.GetCondition(vmcondition.TypeMaintenance, changed.Status.Conditions)

var vmAction VMAction
switch runPolicy {
case virtv2.AlwaysOffPolicy:
vmAction = h.handleAlwaysOffPolicy(ctx, s, kvvmi)
case virtv2.AlwaysOnPolicy:
vmAction, err = h.handleAlwaysOnPolicy(ctx, s, kvvm, kvvmi, isConfigurationApplied, shutdownInfo)
if err != nil {
return err
}
case virtv2.AlwaysOnUnlessStoppedManually:
vmAction, err = h.handleAlwaysOnUnlessStoppedManuallyPolicy(ctx, s, kvvm, kvvmi, isConfigurationApplied, shutdownInfo)
if err != nil {
return err
if maintenance.Status != metav1.ConditionTrue {
switch runPolicy {
case virtv2.AlwaysOffPolicy:
vmAction = h.handleAlwaysOffPolicy(ctx, s, kvvmi)
case virtv2.AlwaysOnPolicy:
vmAction, err = h.handleAlwaysOnPolicy(ctx, s, kvvm, kvvmi, isConfigurationApplied, shutdownInfo)
if err != nil {
return err
}
case virtv2.AlwaysOnUnlessStoppedManually:
vmAction, err = h.handleAlwaysOnUnlessStoppedManuallyPolicy(ctx, s, kvvm, kvvmi, isConfigurationApplied, shutdownInfo)
if err != nil {
return err
}
case virtv2.ManualPolicy:
vmAction = h.handleManualPolicy(ctx, s, kvvm, kvvmi, isConfigurationApplied, shutdownInfo)
}
case virtv2.ManualPolicy:
vmAction = h.handleManualPolicy(ctx, s, kvvm, kvvmi, isConfigurationApplied, shutdownInfo)
} else {
vmAction = Stop
}

switch vmAction {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
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 validators

import (
"context"
"fmt"
"reflect"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

"github.com/deckhouse/virtualization-controller/pkg/controller/conditions"
virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2"
"github.com/deckhouse/virtualization/api/core/v1alpha2/vmcondition"
)

type MaintenanceValidator struct{}

func NewMaintenanceValidator() *MaintenanceValidator {
return &MaintenanceValidator{}
}

func (v *MaintenanceValidator) ValidateCreate(_ context.Context, _ *virtv2.VirtualMachine) (admission.Warnings, error) {
return nil, nil
}

func (v *MaintenanceValidator) ValidateUpdate(_ context.Context, oldVM, newVM *virtv2.VirtualMachine) (admission.Warnings, error) {
maintenance, _ := conditions.GetCondition(vmcondition.TypeMaintenance, oldVM.Status.Conditions)

if maintenance.Status != metav1.ConditionTrue {
return nil, nil
}

if !reflect.DeepEqual(oldVM.Spec, newVM.Spec) {
return nil, fmt.Errorf("spec changes are not allowed while VirtualMachine is in maintenance mode")
}

return nil, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ func SetupController(
blockDeviceService := service.NewBlockDeviceService(client)
vmClassService := service.NewVirtualMachineClassService(client)
handlers := []Handler{
internal.NewMaintenanceHandler(client),
internal.NewDeletionHandler(client),
internal.NewClassHandler(client, recorder),
internal.NewIPAMHandler(netmanager.NewIPAM(), client, recorder),
Expand Down Expand Up @@ -94,7 +95,7 @@ func SetupController(

if err = builder.WebhookManagedBy(mgr).
For(&v1alpha2.VirtualMachine{}).
WithValidator(NewValidator(netmanager.NewIPAM(), client, blockDeviceService, log)).
WithValidator(NewValidator(client, blockDeviceService, log)).
WithDefaulter(NewDefaulter(client, vmClassService, log)).
Complete(); err != nil {
return err
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import (

"github.com/deckhouse/deckhouse/pkg/log"
"github.com/deckhouse/virtualization-controller/pkg/controller/service"
"github.com/deckhouse/virtualization-controller/pkg/controller/vm/internal"
"github.com/deckhouse/virtualization-controller/pkg/controller/vm/internal/defaulter"
"github.com/deckhouse/virtualization-controller/pkg/controller/vm/internal/validators"
virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2"
Expand All @@ -42,9 +41,10 @@ type Validator struct {
log *log.Logger
}

func NewValidator(ipam internal.IPAM, client client.Client, service *service.BlockDeviceService, log *log.Logger) *Validator {
func NewValidator(client client.Client, service *service.BlockDeviceService, log *log.Logger) *Validator {
return &Validator{
validators: []VirtualMachineValidator{
validators.NewMaintenanceValidator(),
validators.NewMetaValidator(client),
validators.NewIPAMValidator(client),
validators.NewBlockDeviceSpecRefsValidator(),
Expand Down
Loading