Skip to content

Commit af99727

Browse files
committed
fix(reset): substitute alias target under merge key
The reset processor stopped dereferencing aliases during its tree walk in 468e314, leaving AliasNodes intact in the resulting Content. The YAML library's merge handler only accepts an AliasNode under `<<` when its target is a MappingNode; alias-to-SequenceNode is rejected even though the spec-defined "sequence of mappings" form works inline. Compose files using `<<: *anchor` against a list-shaped anchor consequently failed to load. Substitute the resolved target only for the `<<` key. Other keys continue to preserve their AliasNode so downstream traversal is unaffected. Fixes docker/compose#13812. Signed-off-by: Guillaume Lours <glours@users.noreply.github.com>
1 parent 4d30738 commit af99727

2 files changed

Lines changed: 87 additions & 1 deletion

File tree

loader/reset.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,12 @@ func (p *ResetProcessor) resolveContainer(node *yaml.Node, path tree.Path) (*yam
190190
if resolved == nil {
191191
continue
192192
}
193-
if v.Kind == yaml.AliasNode {
193+
// Under the merge key `<<`, the YAML library only accepts an
194+
// AliasNode value when its target is a MappingNode. An alias to a
195+
// SequenceNode (the spec-allowed "sequence of mappings" form via an
196+
// anchor) is rejected. Substitute the resolved target so the YAML
197+
// library sees the underlying node directly for merge keys.
198+
if v.Kind == yaml.AliasNode && key != "<<" {
194199
nodes = append(nodes, node.Content[idx-1], v)
195200
} else {
196201
nodes = append(nodes, node.Content[idx-1], resolved)

loader/reset_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,3 +417,84 @@ services:
417417
assert.Check(t, len(p.Services["svc1"].Ports) == 0, "svc1 ports should be reset")
418418
assert.Check(t, len(p.Services["svc2"].Ports) == 0, "svc2 ports should be reset")
419419
}
420+
421+
// TestMergeKeyAliasTargets verifies that a `<<:` merge key accepts alias values
422+
// regardless of whether the anchored target is a mapping or a sequence of mappings.
423+
// Regression for docker/compose#13812: alias-to-sequence used to fail with
424+
// "map merge requires map or sequence of maps as the value" because the YAML
425+
// library only accepts AliasNode→MappingNode at merge sites.
426+
func TestMergeKeyAliasTargets(t *testing.T) {
427+
tests := []struct {
428+
name string
429+
config string
430+
}{
431+
{
432+
name: "alias_to_mapping",
433+
config: `
434+
name: test
435+
x-base: &base
436+
image: nginx
437+
restart: unless-stopped
438+
services:
439+
s1:
440+
<<: *base
441+
`,
442+
},
443+
{
444+
name: "alias_to_sequence_of_mappings",
445+
config: `
446+
name: test
447+
x-list: &alist
448+
- image: nginx
449+
services:
450+
s1:
451+
<<: *alist
452+
`,
453+
},
454+
{
455+
name: "alias_to_sequence_shared_across_services",
456+
config: `
457+
name: test
458+
x-list: &alist
459+
- image: nginx
460+
restart: unless-stopped
461+
services:
462+
s1:
463+
<<: *alist
464+
s2:
465+
<<: *alist
466+
`,
467+
},
468+
{
469+
name: "inline_sequence_of_mappings",
470+
config: `
471+
name: test
472+
x-base: &base
473+
image: nginx
474+
services:
475+
s1:
476+
<<: [*base, {restart: unless-stopped}]
477+
`,
478+
},
479+
}
480+
for _, tt := range tests {
481+
t.Run(tt.name, func(t *testing.T) {
482+
_, err := loadResetYAML(context.TODO(), tt.config)
483+
assert.NilError(t, err)
484+
})
485+
}
486+
}
487+
488+
// TestMergeKeyAliasToScalarRejected verifies that a `<<:` merge key value that
489+
// resolves to a scalar (neither mapping nor sequence of mappings) is still
490+
// rejected. Guards against the fix making the parser too permissive.
491+
func TestMergeKeyAliasToScalarRejected(t *testing.T) {
492+
_, err := loadResetYAML(context.TODO(), `
493+
name: test
494+
x-scalar: &s "not-a-map"
495+
services:
496+
s1:
497+
<<: *s
498+
`)
499+
assert.ErrorContains(t, err, "map merge requires map")
500+
}

0 commit comments

Comments
 (0)