Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
74efc34
Migrate EvictionReconciler to SSA status updates
fwiesel May 5, 2026
35d43a4
Migrate HypervisorController to SSA status updates
fwiesel May 5, 2026
004de66
Migrate HypervisorTaintController to SSA status updates
fwiesel May 5, 2026
40f517b
Migrate HypervisorMaintenanceController to SSA status updates
fwiesel May 5, 2026
723b232
Migrate OnboardingController to SSA status updates
fwiesel May 5, 2026
2f41ef9
Migrate HypervisorOffboardingReconciler to SSA status updates
fwiesel May 5, 2026
35efa4f
Migrate AggregatesController to SSA status updates
fwiesel May 5, 2026
cc33b16
Migrate TraitsController to SSA status updates
fwiesel May 5, 2026
db4720d
Migrate ready.Controller to SSA status updates; remove PatchHyperviso…
fwiesel May 5, 2026
61e89a8
Migrate HypervisorMaintenanceController Eviction creation to SSA
fwiesel May 6, 2026
d06dd1f
Migrate NodeCertificateController Certificate creation to SSA
fwiesel May 6, 2026
0816907
Refactor HypervisorMaintenanceController: pass statusCfg into sub-fun…
fwiesel May 6, 2026
0d19f14
Refactor OnboardingController: statusCfg passed through reconcile chain
fwiesel May 6, 2026
ec266e5
Fix conditions helpers and HypervisorController status apply issues
fwiesel Jul 8, 2026
9aec816
Fix SSA condition seeding: scope each controller to its own conditions
fwiesel Jul 8, 2026
bc85729
Adapt SSA call sites to controller-gen v0.21.0 signatures
fwiesel Jul 9, 2026
6d0a83a
Fix label propagation and forced_down clearing on service enable
fwiesel Jul 9, 2026
df5d971
HypervisorMaintenanceController: delegate SetupWithManager to registe…
fwiesel Jul 9, 2026
c1d98ff
Move InstanceHa Enabling/Disabling to own controller
fwiesel Nov 19, 2025
a3646df
Move Hypervisor disabling to own controller
fwiesel Mar 10, 2026
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
3 changes: 2 additions & 1 deletion api/v1/hypervisor_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ type HypervisorSpec struct {
InstallCertificate bool `json:"installCertificate"`

// +kubebuilder:optional
// +kubebuilder:validation:Enum:="";manual;auto;ha;termination
// +kubebuilder:validation:Enum:="";manual;auto;ha;no-schedule;termination
// Maintenance indicates whether the hypervisor is in maintenance mode.
Maintenance string `json:"maintenance,omitempty"`

Expand Down Expand Up @@ -213,6 +213,7 @@ const (
MaintenanceManual = "manual" // manual maintenance mode by external user
MaintenanceAuto = "auto" // automatic maintenance mode
MaintenanceHA = "ha" // high availability maintenance mode
MaintenanceNoSchedule = "no-schedule" // compute service disabled, no eviction
MaintenanceTermination = "termination" // internal use only, when node is terminating state
)

Expand Down
554 changes: 554 additions & 0 deletions charts/openstack-hypervisor-operator/crds/hypervisor-crd.yaml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ spec:
- manual
- auto
- ha
- no-schedule
- termination
type: string
maintenanceReason:
Expand Down
9 changes: 8 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,6 @@ func main() {
setupLog.Error(err, "unable to create controller", "controller", "Eviction")
os.Exit(1)
}

if err = (&controller.HypervisorOffboardingReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Expand All @@ -313,6 +312,14 @@ func main() {
os.Exit(1)
}

if err = (&controller.HypervisorComputeServiceController{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", controller.HypervisorComputeServiceControllerName)
os.Exit(1)
}

if err = (&controller.OnboardingController{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Expand Down
90 changes: 74 additions & 16 deletions internal/controller/aggregates_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,24 @@ package controller

import (
"context"
"encoding/json"
"errors"
"fmt"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
k8sacmetav1 "k8s.io/client-go/applyconfigurations/meta/v1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
k8sclient "sigs.k8s.io/controller-runtime/pkg/client"

"github.com/gophercloud/gophercloud/v2"

kvmv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1"
apiv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/applyconfigurations/api/v1"
"github.com/cobaltcore-dev/openstack-hypervisor-operator/internal/openstack"
"github.com/cobaltcore-dev/openstack-hypervisor-operator/internal/utils"
)
Expand Down Expand Up @@ -76,17 +80,32 @@ func (ac *AggregatesController) Reconcile(ctx context.Context, req ctrl.Request)
// Apply aggregates to OpenStack and update status
aggregates, err := openstack.ApplyAggregates(ctx, ac.computeClient, hv.Name, desiredAggregateNames)
if err != nil {
// Set error condition with retry on conflict
condition := metav1.Condition{
Type: kvmv1.ConditionTypeAggregatesUpdated,
Status: metav1.ConditionFalse,
Reason: kvmv1.ConditionReasonFailed,
Message: fmt.Errorf("failed to apply aggregates: %w", err).Error(),
// Set error condition, preserving current aggregate ownership
statusCfg := apiv1.HypervisorStatus()
if c := meta.FindStatusCondition(hv.Status.Conditions, kvmv1.ConditionTypeAggregatesUpdated); c != nil {
statusCfg.WithConditions(utils.ConditionFromStatus(*c))
}
utils.SetApplyConfigurationStatusCondition(&statusCfg.Conditions,
*k8sacmetav1.Condition().
WithType(kvmv1.ConditionTypeAggregatesUpdated).
WithStatus(metav1.ConditionFalse).
WithReason(kvmv1.ConditionReasonFailed).
WithMessage(fmt.Errorf("failed to apply aggregates: %w", err).Error()))
if len(hv.Status.Aggregates) > 0 {
aggCfgs := make([]apiv1.AggregateApplyConfiguration, len(hv.Status.Aggregates))
for i, agg := range hv.Status.Aggregates {
a := apiv1.Aggregate().WithName(agg.Name).WithUUID(agg.UUID)
if len(agg.Metadata) > 0 {
a.WithMetadata(agg.Metadata)
}
aggCfgs[i] = *a
}
statusCfg.Aggregates = aggCfgs
}

if err2 := utils.PatchHypervisorStatusWithRetry(ctx, ac.Client, hv.Name, AggregatesControllerName, func(h *kvmv1.Hypervisor) {
meta.SetStatusCondition(&h.Status.Conditions, condition)
}); err2 != nil {
if err2 := ac.Status().Apply(ctx,
apiv1.Hypervisor(hv.Name).WithStatus(statusCfg),
k8sclient.ForceOwnership, k8sclient.FieldOwner(AggregatesControllerName)); err2 != nil {
return ctrl.Result{}, errors.Join(err, err2)
}
return ctrl.Result{}, err
Expand All @@ -106,15 +125,54 @@ func (ac *AggregatesController) Reconcile(ctx context.Context, req ctrl.Request)
return ctrl.Result{}, nil
}

// Patch status with retry on conflict
err := utils.PatchHypervisorStatusWithRetry(ctx, ac.Client, hv.Name, AggregatesControllerName, func(h *kvmv1.Hypervisor) {
if aggregatesChanged {
h.Status.Aggregates = newAggregates
statusCfg := apiv1.HypervisorStatus()
if c := meta.FindStatusCondition(hv.Status.Conditions, kvmv1.ConditionTypeAggregatesUpdated); c != nil {
statusCfg.WithConditions(utils.ConditionFromStatus(*c))
}
utils.SetApplyConfigurationStatusCondition(&statusCfg.Conditions,
*k8sacmetav1.Condition().
WithType(desiredCondition.Type).
WithStatus(desiredCondition.Status).
WithReason(desiredCondition.Reason).
WithMessage(desiredCondition.Message))

if aggregatesChanged && len(newAggregates) > 0 {
aggCfgs := make([]apiv1.AggregateApplyConfiguration, len(newAggregates))
for i, agg := range newAggregates {
a := apiv1.Aggregate().WithName(agg.Name).WithUUID(agg.UUID)
if len(agg.Metadata) > 0 {
a.WithMetadata(agg.Metadata)
}
aggCfgs[i] = *a
}
statusCfg.Aggregates = aggCfgs
}

if err := ac.Status().Apply(ctx,
apiv1.Hypervisor(hv.Name).WithStatus(statusCfg),
k8sclient.ForceOwnership, k8sclient.FieldOwner(AggregatesControllerName)); err != nil {
return ctrl.Result{}, err
}

// The Aggregates field uses omitempty in the generated apply config, so an
// empty slice cannot be sent via SSA. Use a targeted merge patch to clear it.
if aggregatesChanged && len(newAggregates) == 0 {
patch, err := json.Marshal(map[string]any{
"status": map[string]any{
"aggregates": []kvmv1.Aggregate{},
},
})
if err != nil {
return ctrl.Result{}, err
}
fresh := &kvmv1.Hypervisor{}
if err := ac.Get(ctx, k8sclient.ObjectKey{Name: hv.Name}, fresh); err != nil {
return ctrl.Result{}, err
}
meta.SetStatusCondition(&h.Status.Conditions, desiredCondition)
})
return ctrl.Result{}, ac.Status().Patch(ctx, fresh, k8sclient.RawPatch(types.MergePatchType, patch))
}

return ctrl.Result{}, err
return ctrl.Result{}, nil
}

// determineDesiredState returns the desired aggregates and the corresponding condition
Expand Down
Loading
Loading