@@ -431,48 +431,96 @@ func TestApplyTemplatesPerNode_MaintenanceModeOpensFreshClientPerNode(t *testing
431431 }
432432}
433433
434- // TestOpenClientPerNodeMaintenance_NarrowsAndRestoresGlobalNodes verifies
435- // the production maintenance opener narrows GlobalArgs.Nodes to the
436- // iteration's single endpoint while WithClientMaintenance reads it, and
437- // restores the prior value afterwards regardless of whether the action
438- // succeeded. Without this narrowing, WithClientMaintenance would build a
439- // client with every endpoint and gRPC would round-robin
440- // ApplyConfiguration.
434+ // TestOpenClientPerNodeMaintenance_NarrowsAndRestoresGlobalNodes drives
435+ // the real openClientPerNodeMaintenance with an injected
436+ // maintenanceClientFunc fake. The fake captures GlobalArgs.Nodes at the
437+ // moment a real WithClientMaintenance would have read it for endpoint
438+ // resolution. The contract: every iteration narrows GlobalArgs.Nodes to
439+ // exactly the iteration's node, and the prior value is restored after
440+ // the action returns regardless of success. Without the narrowing, the
441+ // real WithClientMaintenance would dial every endpoint at once and gRPC
442+ // would round-robin ApplyConfiguration across them.
441443func TestOpenClientPerNodeMaintenance_NarrowsAndRestoresGlobalNodes (t * testing.T ) {
442444 saved := append ([]string (nil ), GlobalArgs .Nodes ... )
443445 defer func () { GlobalArgs .Nodes = saved }()
444446
445447 GlobalArgs .Nodes = []string {"original-A" , "original-B" }
446448
447- // We can't invoke the real WithClientMaintenance without a Talos
448- // endpoint, but openClientPerNodeMaintenance's narrowing is
449- // observable: stub the action to capture GlobalArgs.Nodes at the
450- // moment WithClientMaintenance reads them. WithClientMaintenance
451- // dials a TCP socket; stubbing it requires a fake. We instead
452- // replicate the narrow/defer/restore logic on an inline maintenance
453- // stub of the same shape.
454- openWithStub := func (node string , action func (ctx context.Context , c * client.Client ) error ) error {
455- savedNodes := append ([]string (nil ), GlobalArgs .Nodes ... )
456- GlobalArgs .Nodes = []string {node }
457- defer func () { GlobalArgs .Nodes = savedNodes }()
458-
459- // In production WithClientMaintenance reads GlobalArgs.Nodes here.
460- // Capture the value and exit without doing real network IO.
461- if got := GlobalArgs .Nodes ; len (got ) != 1 || got [0 ] != node {
462- t .Errorf ("expected GlobalArgs.Nodes pinned to %q during open; got %v" , node , got )
463- }
449+ type call struct {
450+ fingerprints []string
451+ nodesAtCall []string
452+ }
453+ var calls []call
454+
455+ fakeMaintenance := func (fingerprints []string , action func (ctx context.Context , c * client.Client ) error ) error {
456+ // WithClientMaintenance reads GlobalArgs.Nodes for its endpoints
457+ // at this point. Snapshot the value so the test can inspect it.
458+ calls = append (calls , call {
459+ fingerprints : append ([]string (nil ), fingerprints ... ),
460+ nodesAtCall : append ([]string (nil ), GlobalArgs .Nodes ... ),
461+ })
464462 return action (context .Background (), nil )
465463 }
466464
465+ openClient := openClientPerNodeMaintenance ([]string {"fp-1" }, fakeMaintenance )
466+
467467 for _ , node := range []string {"10.0.0.1" , "10.0.0.2" } {
468- if err := openWithStub (node , func (_ context.Context , _ * client.Client ) error { return nil }); err != nil {
469- t .Fatalf ("openWithStub (%q): %v" , node , err )
468+ if err := openClient (node , func (_ context.Context , _ * client.Client ) error { return nil }); err != nil {
469+ t .Fatalf ("openClient (%q): %v" , node , err )
470470 }
471471 }
472472
473473 if ! slices .Equal (GlobalArgs .Nodes , []string {"original-A" , "original-B" }) {
474474 t .Errorf ("GlobalArgs.Nodes not restored after maintenance loop: got %v" , GlobalArgs .Nodes )
475475 }
476+ if len (calls ) != 2 {
477+ t .Fatalf ("maintenance fake should have been called twice, got %d times" , len (calls ))
478+ }
479+ for i , want := range []string {"10.0.0.1" , "10.0.0.2" } {
480+ if ! slices .Equal (calls [i ].nodesAtCall , []string {want }) {
481+ t .Errorf ("call %d: GlobalArgs.Nodes at WithClientMaintenance time = %v, want [%q]" , i , calls [i ].nodesAtCall , want )
482+ }
483+ if ! slices .Equal (calls [i ].fingerprints , []string {"fp-1" }) {
484+ t .Errorf ("call %d: fingerprints passed through = %v, want [\" fp-1\" ]" , i , calls [i ].fingerprints )
485+ }
486+ }
487+ }
488+
489+ // TestApplyTemplatesPerNode_AuthModeUsesSingleNodeMetadataKey pins the
490+ // gRPC metadata key the auth-mode opener writes. WithNode sets "node"
491+ // (single-target proxy); WithNodes sets "nodes" (apid aggregation).
492+ // engine.Render's FailIfMultiNodes guard treats len("nodes") > 1 as the
493+ // multi-node case, so single-target metadata under "node" passes
494+ // trivially. A future refactor that swaps WithNode back to WithNodes
495+ // would slip past nodesFromOutgoingCtx (which reads either key) — this
496+ // assertion catches that regression directly.
497+ func TestApplyTemplatesPerNode_AuthModeUsesSingleNodeMetadataKey (t * testing.T ) {
498+ dir := t .TempDir ()
499+ configFile := filepath .Join (dir , "node.yaml" )
500+ if err := os .WriteFile (configFile , []byte ("# talm: nodes=[\" a\" ]\n " ), 0o644 ); err != nil {
501+ t .Fatalf ("write configFile: %v" , err )
502+ }
503+
504+ const node = "10.0.0.1"
505+ render := func (ctx context.Context , _ * client.Client , _ engine.Options ) ([]byte , error ) {
506+ md , ok := metadata .FromOutgoingContext (ctx )
507+ if ! ok {
508+ t .Fatal ("expected outgoing metadata on per-iteration ctx" )
509+ }
510+ if got := md .Get ("node" ); ! slices .Equal (got , []string {node }) {
511+ t .Errorf (`metadata key "node" = %v, want [%q]` , got , node )
512+ }
513+ if got := md .Get ("nodes" ); len (got ) != 0 {
514+ t .Errorf (`metadata key "nodes" must be unset for single-target apply, got %v` , got )
515+ }
516+ return []byte ("version: v1alpha1\n machine:\n type: worker\n " ), nil
517+ }
518+ apply := func (_ context.Context , _ * client.Client , _ []byte ) error { return nil }
519+
520+ openClient := openClientPerNodeAuth (context .Background (), nil )
521+ if err := applyTemplatesPerNode (engine.Options {}, configFile , []string {node }, openClient , render , apply ); err != nil {
522+ t .Fatalf ("applyTemplatesPerNode: %v" , err )
523+ }
476524}
477525
478526// TestTemplateAndApplyDiverge_NodeBodyOverlayLimitation pins a known
0 commit comments