@@ -23,6 +23,7 @@ import (
2323 "fmt"
2424 "io"
2525 "os"
26+ "path"
2627 "path/filepath"
2728 "reflect"
2829 "regexp"
@@ -31,7 +32,6 @@ import (
3132 "strings"
3233
3334 "github.com/compose-spec/compose-go/v2/dotenv"
34- "github.com/sirupsen/logrus"
3535 "github.com/compose-spec/compose-go/v2/format"
3636 interp "github.com/compose-spec/compose-go/v2/interpolation"
3737 "github.com/compose-spec/compose-go/v2/override"
@@ -40,6 +40,7 @@ import (
4040 "github.com/compose-spec/compose-go/v2/tree"
4141 "github.com/compose-spec/compose-go/v2/types"
4242 "github.com/compose-spec/compose-go/v2/validation"
43+ "github.com/sirupsen/logrus"
4344 "go.yaml.in/yaml/v4"
4445)
4546
@@ -85,15 +86,13 @@ type ComposeModel struct {
8586func init () {
8687 // Wire up the volume-parsing hook so that types.ServiceVolumeConfig.UnmarshalYAML
8788 // can parse short syntax without importing the format package directly.
88- types .ParseVolumeFunc = func (s string ) (types.ServiceVolumeConfig , error ) {
89- return format .ParseVolume (s )
90- }
89+ types .ParseVolumeFunc = format .ParseVolume
9190}
9291
9392// LoadLazyModel parses compose files into raw yaml.Node layers without
9493// performing interpolation or normalization. The resulting ComposeModel
9594// can later be materialized into a types.Project by calling Resolve().
96- func LoadLazyModel (ctx context.Context , configDetails types.ConfigDetails , options ... func (* Options )) (* ComposeModel , error ) {
95+ func LoadLazyModel (_ context.Context , configDetails types.ConfigDetails , options ... func (* Options )) (* ComposeModel , error ) {
9796 opts := ToOptions (& configDetails , options )
9897
9998 if len (configDetails .ConfigFiles ) < 1 {
@@ -105,9 +104,9 @@ func LoadLazyModel(ctx context.Context, configDetails types.ConfigDetails, optio
105104 }
106105
107106 model := & ComposeModel {
108- configDetails : configDetails ,
109- opts : opts ,
110- nodeContexts : make (map [* yaml.Node ]* NodeContext ),
107+ configDetails : configDetails ,
108+ opts : opts ,
109+ nodeContexts : make (map [* yaml.Node ]* NodeContext ),
111110 serviceWorkDirs : make (map [string ]string ),
112111 }
113112
@@ -242,7 +241,7 @@ func checkDuplicateKeys(node *yaml.Node) error {
242241// This is the point where all deferred processing happens:
243242// extends resolution, includes loading, merging, interpolation,
244243// type casting, and decoding into Go structs.
245- func (m * ComposeModel ) Resolve () (* types.Project , error ) {
244+ func (m * ComposeModel ) Resolve () (* types.Project , error ) { //nolint:gocyclo
246245 // 1. Process extends on raw nodes (before interpolation, same as existing pipeline)
247246 if ! m .opts .SkipExtends {
248247 for _ , layer := range m .layers {
@@ -372,9 +371,7 @@ func (m *ComposeModel) Resolve() (*types.Project, error) {
372371
373372 // 7. Normalization (default network, resource names, build defaults, etc.)
374373 if ! m .opts .SkipNormalization {
375- if err := normalizeProject (project , m .opts ); err != nil {
376- return nil , err
377- }
374+ normalizeProject (project )
378375 }
379376
380377 // 7a. Always resolve environment references in secrets/configs
@@ -429,7 +426,7 @@ func (m *ComposeModel) Resolve() (*types.Project, error) {
429426// interpolateTree walks the yaml.Node tree and interpolates scalar values
430427// using per-node context from nodeContexts. Nodes not found in the map
431428// inherit context from the nearest registered ancestor during the walk.
432- func (m * ComposeModel ) interpolateTree (node * yaml.Node , path tree.Path , inherited * NodeContext ) error {
429+ func (m * ComposeModel ) interpolateTree (node * yaml.Node , p tree.Path , inherited * NodeContext ) error {
433430 if node == nil {
434431 return nil
435432 }
@@ -443,7 +440,7 @@ func (m *ComposeModel) interpolateTree(node *yaml.Node, path tree.Path, inherite
443440 switch node .Kind {
444441 case yaml .DocumentNode :
445442 for _ , child := range node .Content {
446- if err := m .interpolateTree (child , path , ctx ); err != nil {
443+ if err := m .interpolateTree (child , p , ctx ); err != nil {
447444 return err
448445 }
449446 }
@@ -463,28 +460,28 @@ func (m *ComposeModel) interpolateTree(node *yaml.Node, path tree.Path, inherite
463460 pairCtx = c
464461 }
465462
466- next := path .Next (keyNode .Value )
463+ next := p .Next (keyNode .Value )
467464 if err := m .interpolateTree (valNode , next , pairCtx ); err != nil {
468465 return err
469466 }
470467 }
471468
472469 case yaml .SequenceNode :
473470 for _ , child := range node .Content {
474- if err := m .interpolateTree (child , path .Next (tree .PathMatchList ), ctx ); err != nil {
471+ if err := m .interpolateTree (child , p .Next (tree .PathMatchList ), ctx ); err != nil {
475472 return err
476473 }
477474 }
478475
479476 case yaml .ScalarNode :
480- return m .interpolateScalar (node , path , ctx )
477+ return m .interpolateScalar (node , p , ctx )
481478 }
482479
483480 return nil
484481}
485482
486483// interpolateScalar substitutes variables and applies type casting on a single scalar node.
487- func (m * ComposeModel ) interpolateScalar (node * yaml.Node , path tree.Path , ctx * NodeContext ) error {
484+ func (m * ComposeModel ) interpolateScalar (node * yaml.Node , p tree.Path , ctx * NodeContext ) error {
488485 if node .Tag != "!!str" && node .Tag != "" && ! strings .Contains (node .Value , "$" ) {
489486 return nil
490487 }
@@ -505,7 +502,7 @@ func (m *ComposeModel) interpolateScalar(node *yaml.Node, path tree.Path, ctx *N
505502 // Type casting based on tree path
506503 var caster interp.Cast
507504 for pattern , c := range interpolateTypeCastMapping {
508- if path .Matches (pattern ) {
505+ if p .Matches (pattern ) {
509506 caster = c
510507 break
511508 }
@@ -615,7 +612,7 @@ func (m *ComposeModel) resolveServiceExtends(layer *Layer, services *yaml.Node,
615612 // cycle detection using file:service identifiers
616613 chainID := layer .Context .Source + ":" + name
617614 if slices .Contains (chain , chainID ) {
618- return fmt .Errorf ("Circular reference with extends" )
615+ return fmt .Errorf ("circular reference with extends" )
619616 }
620617 chain = append (chain , chainID )
621618
@@ -812,7 +809,7 @@ func (m *ComposeModel) applyIncludeNodes() error {
812809 return nil
813810}
814811
815- func (m * ComposeModel ) loadIncludeEntry (parent * Layer , entry * yaml.Node ) ([]* Layer , error ) {
812+ func (m * ComposeModel ) loadIncludeEntry (parent * Layer , entry * yaml.Node ) ([]* Layer , error ) { //nolint:gocyclo
816813 var paths []string
817814 var projectDir string
818815 var envFiles []string
@@ -859,15 +856,16 @@ func (m *ComposeModel) loadIncludeEntry(parent *Layer, entry *yaml.Node) ([]*Lay
859856 for i , p := range paths {
860857 resolved := false
861858 for _ , loader := range m .opts .RemoteResourceLoaders () {
862- if loader .Accept (p ) {
863- absPath , loadErr := loader .Load (context .TODO (), p )
864- if loadErr != nil {
865- return nil , types .WrapNodeError (entry , fmt .Errorf ("loading include %s: %w" , p , loadErr ))
866- }
867- paths [i ] = absPath
868- resolved = true
869- break
859+ if ! loader .Accept (p ) {
860+ continue
870861 }
862+ absPath , loadErr := loader .Load (context .TODO (), p )
863+ if loadErr != nil {
864+ return nil , types .WrapNodeError (entry , fmt .Errorf ("loading include %s: %w" , p , loadErr ))
865+ }
866+ paths [i ] = absPath
867+ resolved = true
868+ break
871869 }
872870 if ! resolved && ! filepath .IsAbs (p ) {
873871 paths [i ] = filepath .Join (parent .Context .WorkingDir , p )
@@ -1022,13 +1020,13 @@ func addBuildContextDefault(svc *yaml.Node) {
10221020// resolveServiceNodePaths adjusts relative paths in a service yaml.Node to be
10231021// expressed relative to workDir. This is used for extends from external files
10241022// to make paths relative to the main project directory.
1025- func resolveServiceNodePaths (svc * yaml.Node , workDir string ) {
1023+ func resolveServiceNodePaths (svc * yaml.Node , workDir string ) { //nolint:gocyclo
10261024 if svc == nil || svc .Kind != yaml .MappingNode || workDir == "." {
10271025 return
10281026 }
10291027
10301028 absNodePath := func (p string ) string {
1031- if filepath .IsAbs (p ) || p == "" {
1029+ if filepath .IsAbs (p ) || path . IsAbs ( p ) || p == "" {
10321030 return p
10331031 }
10341032 return filepath .Join (workDir , p )
@@ -1037,12 +1035,13 @@ func resolveServiceNodePaths(svc *yaml.Node, workDir string) {
10371035 // build.context
10381036 _ , build := override .FindKey (svc , "build" )
10391037 if build != nil {
1040- if build .Kind == yaml .ScalarNode {
1038+ switch build .Kind {
1039+ case yaml .ScalarNode :
10411040 // short syntax: build: ./path
10421041 if ! strings .Contains (build .Value , "://" ) {
10431042 build .Value = absNodePath (build .Value )
10441043 }
1045- } else if build . Kind == yaml .MappingNode {
1044+ case yaml .MappingNode :
10461045 _ , ctx := override .FindKey (build , "context" )
10471046 if ctx != nil && ctx .Kind == yaml .ScalarNode && ! strings .Contains (ctx .Value , "://" ) {
10481047 ctx .Value = absNodePath (ctx .Value )
@@ -1079,18 +1078,19 @@ func resolveServiceNodePaths(svc *yaml.Node, workDir string) {
10791078 _ , volumes := override .FindKey (svc , "volumes" )
10801079 if volumes != nil && volumes .Kind == yaml .SequenceNode {
10811080 for i , item := range volumes .Content {
1082- if item .Kind == yaml .MappingNode {
1081+ switch item .Kind {
1082+ case yaml .MappingNode :
10831083 _ , vtype := override .FindKey (item , "type" )
10841084 if vtype != nil && vtype .Value == "bind" {
10851085 _ , src := override .FindKey (item , "source" )
10861086 if src != nil && src .Kind == yaml .ScalarNode {
10871087 src .Value = absNodePath (src .Value )
10881088 }
10891089 }
1090- } else if item . Kind == yaml .ScalarNode {
1090+ case yaml .ScalarNode :
10911091 // Short syntax: parse, resolve source, convert to long syntax mapping
10921092 vol , err := format .ParseVolume (item .Value )
1093- if err == nil && vol .Type == types .VolumeTypeBind && vol .Source != "" && ! filepath .IsAbs (vol .Source ) {
1093+ if err == nil && vol .Type == types .VolumeTypeBind && vol .Source != "" && ! filepath .IsAbs (vol .Source ) && ! path . IsAbs ( vol . Source ) {
10941094 vol .Source = absNodePath (vol .Source )
10951095 // Convert to long syntax mapping node to preserve bind type
10961096 trueNode := & yaml.Node {Kind : yaml .ScalarNode , Tag : "!!bool" , Value : "true" }
@@ -1252,7 +1252,7 @@ func resolveLayerNodePaths(node *yaml.Node, workDir string) {
12521252 }
12531253
12541254 absPath := func (p string ) string {
1255- if filepath .IsAbs (p ) || p == "" {
1255+ if filepath .IsAbs (p ) || path . IsAbs ( p ) || p == "" {
12561256 return p
12571257 }
12581258 return filepath .Join (workDir , p )
@@ -1299,16 +1299,20 @@ func resolveLayerNodePaths(node *yaml.Node, workDir string) {
12991299}
13001300
13011301// processProjectExtensions converts raw extension values to registered Go types.
1302- // This mirrors the old processExtensions that operated on map[string]any.
13031302func processProjectExtensions (project * types.Project , known map [string ]any ) error {
13041303 convertExtensions := func (ext types.Extensions ) error {
13051304 for name , val := range ext {
13061305 typ , ok := known [name ]
13071306 if ! ok {
13081307 continue
13091308 }
1309+ // Marshal the raw value to yaml, then unmarshal into the target type
1310+ b , err := yaml .Marshal (val )
1311+ if err != nil {
1312+ return fmt .Errorf ("converting extension %s: %w" , name , err )
1313+ }
13101314 target := reflect .New (reflect .TypeOf (typ )).Interface ()
1311- if err := Transform ( val , target ); err != nil {
1315+ if err := yaml . Unmarshal ( b , target ); err != nil {
13121316 return fmt .Errorf ("converting extension %s: %w" , name , err )
13131317 }
13141318 ext [name ] = reflect .ValueOf (target ).Elem ().Interface ()
0 commit comments