Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions internal/controller/core/openstackversion_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,75 @@ func (r *OpenStackVersionReconciler) Reconcile(ctx context.Context, req ctrl.Req
Log.Info("Waiting on OVN Dataplane updates to complete")
return ctrl.Result{}, nil
}

// When the OVN controller image is the same between the deployed
// version and the target version, the image comparison above always
// passes because the nodeset already has the matching image from
// the previous update. In this case we need additional checks to
// confirm the OVN dataplane deployment for this update cycle has
// actually completed.
//
// We use the saved condition state (from before Init reset) to
// track whether we have observed a running OVN deployment during
// this update cycle:
// - If we see a running OVN deployment now: set condition False
// (RequestedReason) to record that we observed one
// - If no running OVN deployment AND the previous condition was
// False/RequestedReason: the deployment we saw previously has
// completed → proceed (fall through to set True)
// - If no running OVN deployment AND the previous condition was
// NOT False/RequestedReason (e.g. still Unknown from Init):
// we haven't seen a deployment yet → keep waiting
//
// When the image differs between versions, the image match alone
// is sufficient proof that a deployment updated it, since the
// nodeset's ContainerImages are only set on successful completion.
deployedDefaults, hasDeployedDefaults := instance.Status.ContainerImageVersionDefaults[*instance.Status.DeployedVersion]
if hasDeployedDefaults &&
deployedDefaults.OvnControllerImage != nil &&
instance.Status.ContainerImages.OvnControllerImage != nil &&
*deployedDefaults.OvnControllerImage == *instance.Status.ContainerImages.OvnControllerImage {

ovnDeploymentRunning, err := openstack.IsDataplaneDeploymentRunningForContainerImage(
ctx, versionHelper, instance.Namespace, dataplaneNodesets, "OvnControllerImage")
if err != nil {
return ctrl.Result{}, err
}

if ovnDeploymentRunning {
// OVN deployment is actively running — record this in
// the condition so we can detect its completion later.
instance.Status.Conditions.Set(condition.FalseCondition(
corev1beta1.OpenStackVersionMinorUpdateOVNDataplane,
condition.RequestedReason,
condition.SeverityInfo,
corev1beta1.OpenStackVersionMinorUpdateReadyRunningMessage))
Log.Info("Waiting on OVN Dataplane deployment to complete (OVN image unchanged between versions)")
return ctrl.Result{}, nil
}

// No OVN deployment running. Check the saved condition state
// from the previous reconciliation to determine if we ever
// observed one running during this update cycle.
prevOvnDataplaneCond := savedConditions.Get(corev1beta1.OpenStackVersionMinorUpdateOVNDataplane)
if prevOvnDataplaneCond == nil ||
prevOvnDataplaneCond.Reason != condition.RequestedReason {
// We have never observed a running OVN deployment in
// this update cycle — the deployment has not been
// created yet. Keep waiting.
instance.Status.Conditions.Set(condition.FalseCondition(
corev1beta1.OpenStackVersionMinorUpdateOVNDataplane,
condition.InitReason,
condition.SeverityInfo,
corev1beta1.OpenStackVersionMinorUpdateReadyRunningMessage))
Log.Info("Waiting for OVN Dataplane deployment to be created (OVN image unchanged between versions)")
return ctrl.Result{}, nil
}
// Previously saw a running OVN deployment (condition was
// False/RequestedReason), now no OVN deployment is running
// → the deployment has completed. Fall through to set True.
Log.Info("OVN Dataplane deployment completed (OVN image unchanged between versions)")
}
}
instance.Status.Conditions.MarkTrue(
corev1beta1.OpenStackVersionMinorUpdateOVNDataplane,
Expand Down
80 changes: 80 additions & 0 deletions internal/openstack/dataplane.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package openstack

import (
"context"
"slices"

"github.com/openstack-k8s-operators/lib-common/modules/common/helper"
corev1beta1 "github.com/openstack-k8s-operators/openstack-operator/api/core/v1beta1"

dataplanev1 "github.com/openstack-k8s-operators/openstack-operator/api/dataplane/v1beta1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)

Expand Down Expand Up @@ -58,6 +60,84 @@ func DataplaneNodesetsOVNControllerImagesMatch(version *corev1beta1.OpenStackVer
return true
}

// IsDataplaneDeploymentRunningForContainerImage checks whether any in-progress
// OpenStackDataPlaneDeployment is deploying a service that manages the given
// containerImageField (e.g. "OvnControllerImage"). It resolves which services
// each deployment runs (from ServicesOverride or the nodeset's service list)
// and inspects the service's ContainerImageFields to determine if it manages
// the specified container image.
func IsDataplaneDeploymentRunningForContainerImage(
ctx context.Context,
h *helper.Helper,
namespace string,
dataplaneNodesets *dataplanev1.OpenStackDataPlaneNodeSetList,
containerImageField string,
) (bool, error) {
// List all deployments in the namespace
deployments := &dataplanev1.OpenStackDataPlaneDeploymentList{}
opts := []client.ListOption{
client.InNamespace(namespace),
}
err := h.GetClient().List(ctx, deployments, opts...)
if err != nil {
return false, err
}

// Build a map of nodeset name -> nodeset for quick lookup
nodesetMap := make(map[string]*dataplanev1.OpenStackDataPlaneNodeSet, len(dataplaneNodesets.Items))
for i := range dataplaneNodesets.Items {
nodesetMap[dataplaneNodesets.Items[i].Name] = &dataplaneNodesets.Items[i]
}

// Cache service lookups to avoid repeated API calls
serviceCache := make(map[string]*dataplanev1.OpenStackDataPlaneService)

for _, deployment := range deployments.Items {
// Skip completed deployments
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There could be earlier failed deployments containing ovn. We should check if there is a running deployment. Also, I think probably a would still be a race here.

if deployment.Status.Deployed {
continue
}

// Determine which services this deployment runs for each of its nodesets
for _, nodesetName := range deployment.Spec.NodeSets {
nodeset, exists := nodesetMap[nodesetName]
if !exists || len(nodeset.Spec.Nodes) == 0 {
continue
}

var services []string
if len(deployment.Spec.ServicesOverride) != 0 {
services = deployment.Spec.ServicesOverride
} else {
services = nodeset.Spec.Services
}

for _, serviceName := range services {
svc, cached := serviceCache[serviceName]
if !cached {
foundService := &dataplanev1.OpenStackDataPlaneService{}
err := h.GetClient().Get(ctx, types.NamespacedName{
Name: serviceName,
Namespace: namespace,
}, foundService)
if err != nil {
// Service not found — skip it
continue
}
svc = foundService
serviceCache[serviceName] = svc
}

if slices.Contains(svc.Spec.ContainerImageFields, containerImageField) {
return true, nil
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably just check serviceType 'ovn' and avoid the harcoded check for the image name.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there always a service type, which is stable and not custom?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, serviceType is always fixed and the service name can change.

}
}
}
}

return false, nil
}

// DataplaneNodesetsDeployed returns true if all nodesets are deployed with the latest version
func DataplaneNodesetsDeployed(version *corev1beta1.OpenStackVersion, dataplaneNodesets *dataplanev1.OpenStackDataPlaneNodeSetList) bool {
for _, nodeset := range dataplaneNodesets.Items {
Expand Down
Loading