@@ -21,6 +21,7 @@ import (
2121 "context"
2222 "errors"
2323 "fmt"
24+ "slices"
2425 "time"
2526
2627 "github.com/go-playground/validator/v10"
@@ -253,11 +254,10 @@ func (r *OpenStackDataPlaneDeploymentReconciler) Reconcile(ctx context.Context,
253254 shouldRequeue := false
254255 haveError := false
255256 deploymentErrMsg := ""
256- var nodesetServiceMap map [string ][]string
257- var serviceCache * deployment.ServiceCache
257+ var servicePlan * deployment.ServicePlan
258258 backoffLimitReached := false
259259
260- if nodesetServiceMap , serviceCache , err = deployment .DedupeServices (ctx , helper , nodeSets .Items ,
260+ if servicePlan , err = deployment .DedupeServices (ctx , helper , nodeSets .Items ,
261261 instance .Spec .ServicesOverride ); err != nil {
262262 util .LogErrorForObject (helper , err , "OpenStackDeployment error for deployment" , instance )
263263 var missingServiceErr * deployment.MissingServiceError
@@ -304,22 +304,14 @@ func (r *OpenStackDataPlaneDeploymentReconciler) Reconcile(ctx context.Context,
304304 return ctrl.Result {}, err
305305 }
306306
307- // Deploy each nodeSet
308- // The loop starts and checks NodeSet deployments sequentially. However, after they
309- // are started, they are running in parallel, since the loop does not wait
310- // for the first started NodeSet to finish before starting the next.
311- if ! instance .Spec .UseParallelExecution {
312- Log .Info ("useParallelExecution is not set to true, services will run sequentially in list order and service dependencies (dependsOn) are ignored" )
313- }
314- for _ , nodeSet := range nodeSets .Items {
307+ // Built up front: the deployment starts its deployment scoped services itself,
308+ // independent of which nodeset reconciles first.
309+ deployers := make (map [string ]* deployment.Deployer , len (nodeSets .Items ))
310+ serviceLevels := make (map [string ][][]string , len (nodeSets .Items ))
311+
312+ for i := range nodeSets .Items {
313+ nodeSet := & nodeSets .Items [i ]
315314
316- Log .Info ("Deploying NodeSet" , "nodeSet" , nodeSet .Name )
317- Log .Info ("Set Status.Deployed to false" , "instance" , instance )
318- instance .Status .Deployed = false
319- Log .Info ("Set DeploymentReadyCondition false" )
320- instance .Status .Conditions .MarkFalse (
321- condition .DeploymentReadyCondition , condition .RequestedReason ,
322- condition .SeverityInfo , condition .DeploymentReadyRunningMessage )
323315 ansibleEESpec := nodeSet .GetAnsibleEESpec ()
324316 ansibleEESpec .AnsibleTags = instance .Spec .AnsibleTags
325317 ansibleEESpec .AnsibleSkipTags = instance .Spec .AnsibleSkipTags
@@ -334,22 +326,22 @@ func (r *OpenStackDataPlaneDeploymentReconciler) Reconcile(ctx context.Context,
334326 }
335327 }
336328
337- deployer := deployment.Deployer {
329+ deployers [ nodeSet . Name ] = & deployment.Deployer {
338330 Ctx : ctx ,
339331 Helper : helper ,
340- NodeSet : & nodeSet ,
332+ NodeSet : nodeSet ,
341333 Deployment : instance ,
342334 Status : & instance .Status ,
343335 AeeSpec : & ansibleEESpec ,
344336 InventorySecrets : globalInventorySecrets ,
345337 AnsibleSSHPrivateKeySecrets : globalSSHKeySecrets ,
346- ServiceCache : serviceCache ,
338+ ServiceCache : servicePlan . Cache ,
347339 Version : version ,
348340 }
349341
350- var serviceLevels [][]string
351342 if instance .Spec .UseParallelExecution {
352- serviceLevels , err = deployment .BuildServiceLevels (ctx , helper , serviceCache , nodesetServiceMap [nodeSet .Name ])
343+ serviceLevels [nodeSet .Name ], err = deployment .BuildServiceLevels (
344+ ctx , helper , servicePlan .Cache , servicePlan .Services [nodeSet .Name ])
353345 if err != nil {
354346 util .LogErrorForObject (helper , err , fmt .Sprintf ("error building service dependency graph for NodeSet %s" , nodeSet .Name ), instance )
355347 graphErrMsg := fmt .Sprintf ("nodeSet: %s error: %s" , nodeSet .Name , err .Error ())
@@ -370,14 +362,52 @@ func (r *OpenStackDataPlaneDeploymentReconciler) Reconcile(ctx context.Context,
370362 return ctrl.Result {}, fmt .Errorf ("error building service dependency graph for nodeset %s: %w" , nodeSet .Name , err )
371363 }
372364 } else {
373- serviceLevels = deployment .SerialServiceLevels (nodesetServiceMap [nodeSet .Name ])
365+ serviceLevels [ nodeSet . Name ] = deployment .SerialServiceLevels (servicePlan . Services [nodeSet .Name ])
374366 }
367+ deployers [nodeSet .Name ].AllServices = slices .Concat (serviceLevels [nodeSet .Name ]... )
368+ }
369+
370+ // Start them only once every listing nodeset reached its barrier, else a
371+ // nodeset advances while the shared Job still runs against its hosts.
372+ if err := r .deployGlobalServices (ctx , helper , instance , servicePlan , serviceLevels , deployers ); err != nil {
373+ return ctrl.Result {}, err
374+ }
375+
376+ // Deploy each nodeSet
377+ // The loop starts and checks NodeSet deployments sequentially. However, after they
378+ // are started, they are running in parallel, since the loop does not wait
379+ // for the first started NodeSet to finish before starting the next.
380+ if ! instance .Spec .UseParallelExecution {
381+ Log .Info ("useParallelExecution is not set to true, services will run sequentially in list order and service dependencies (dependsOn) are ignored" )
382+ }
383+ for i := range nodeSets .Items {
384+ nodeSet := & nodeSets .Items [i ]
385+
386+ Log .Info ("Deploying NodeSet" , "nodeSet" , nodeSet .Name )
387+ Log .Info ("Set Status.Deployed to false" , "instance" , instance )
388+ instance .Status .Deployed = false
389+ Log .Info ("Set DeploymentReadyCondition false" )
390+ instance .Status .Conditions .MarkFalse (
391+ condition .DeploymentReadyCondition , condition .RequestedReason ,
392+ condition .SeverityInfo , condition .DeploymentReadyRunningMessage )
375393
376394 var deployResult * ctrl.Result
377- deployResult , err = deployer . Deploy (serviceLevels )
395+ deployResult , err = deployers [ nodeSet . Name ]. Deploy (serviceLevels [ nodeSet . Name ] )
378396
379397 nsConditions := instance .Status .NodeSetConditions [nodeSet .Name ]
380398 nsConditions .Set (nsConditions .Mirror (dataplanev1 .NodeSetDeploymentReadyCondition ))
399+ // A barrier wait sets no service condition, so surface it on the nodeset.
400+ if err == nil {
401+ if blockedOn := deployers [nodeSet .Name ].BlockedOn ; blockedOn != "" {
402+ nsConditions .Set (condition .FalseCondition (
403+ dataplanev1 .NodeSetDeploymentReadyCondition ,
404+ condition .RequestedReason ,
405+ condition .SeverityInfo ,
406+ "%s" , fmt .Sprintf ("%s: it runs once for the whole deployment" ,
407+ fmt .Sprintf (dataplanev1 .NodeSetServiceDeploymentReadyWaitingMessage , blockedOn ))))
408+ }
409+ }
410+ instance .Status .NodeSetConditions [nodeSet .Name ] = nsConditions
381411
382412 if err != nil {
383413 util .LogErrorForObject (helper , err , fmt .Sprintf ("OpenStackDeployment error for NodeSet %s" , nodeSet .Name ), instance )
@@ -445,6 +475,64 @@ func (r *OpenStackDataPlaneDeploymentReconciler) Reconcile(ctx context.Context,
445475 return ctrl.Result {}, nil
446476}
447477
478+ // deployGlobalServices starts the single AnsibleEE Job of every deployment
479+ // scoped service whose barrier has cleared.
480+ func (r * OpenStackDataPlaneDeploymentReconciler ) deployGlobalServices (
481+ ctx context.Context ,
482+ helper * helper.Helper ,
483+ instance * dataplanev1.OpenStackDataPlaneDeployment ,
484+ servicePlan * deployment.ServicePlan ,
485+ serviceLevels map [string ][][]string ,
486+ deployers map [string ]* deployment.Deployer ,
487+ ) error {
488+ Log := r .GetLogger (ctx )
489+ jobState := deployment.JobState {}
490+
491+ for _ , serviceName := range servicePlan .GlobalServices {
492+ gateOpen , err := deployment .GlobalServiceGateOpen (
493+ ctx , helper , servicePlan , instance , serviceName , serviceLevels , jobState )
494+ if err != nil {
495+ return err
496+ }
497+ if ! gateOpen {
498+ Log .Info ("Deployment scoped service is behind a barrier that has not cleared" ,
499+ "service" , serviceName , "nodeSets" , servicePlan .NodesetsWithGlobalService [serviceName ])
500+ continue
501+ }
502+
503+ foundService , err := servicePlan .Cache .Get (ctx , helper , serviceName )
504+ if err != nil {
505+ instance .Status .Conditions .MarkFalse (
506+ condition .InputReadyCondition ,
507+ condition .ErrorReason ,
508+ condition .SeverityError ,
509+ dataplanev1 .ServiceErrorMessage ,
510+ err .Error ())
511+ return err
512+ }
513+
514+ listingNodeSets := servicePlan .NodesetsWithGlobalService [serviceName ]
515+ if len (listingNodeSets ) == 0 {
516+ continue
517+ }
518+ // Any listing nodeset works; it only supplies the Job labels and mounts.
519+ if err := deployers [listingNodeSets [0 ]].DeployGlobalService (foundService ); err != nil {
520+ util .LogErrorForObject (helper , err ,
521+ fmt .Sprintf ("Unable to execute deployment scoped service %s" , serviceName ), instance )
522+ instance .Status .Conditions .MarkFalse (
523+ condition .DeploymentReadyCondition ,
524+ condition .ErrorReason ,
525+ condition .SeverityError ,
526+ condition .DeploymentReadyErrorMessage ,
527+ err .Error ())
528+ return err
529+ }
530+ Log .Info ("Started deployment scoped service" , "service" , serviceName , "nodeSets" , listingNodeSets )
531+ }
532+
533+ return nil
534+ }
535+
448536// GetService retrieves a service for the OpenStackDataPlaneDeployment
449537func (r * OpenStackDataPlaneDeploymentReconciler ) GetService (
450538 ctx context.Context ,
0 commit comments