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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/buger/goterm v1.0.4
github.com/compose-spec/compose-go/v2 v2.14.0
github.com/compose-spec/compose-go/v2 v2.14.1-0.20260825154407-6c1c2d727681
github.com/containerd/console v1.0.5
github.com/containerd/containerd/v2 v2.3.3
github.com/containerd/errdefs v1.0.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
github.com/compose-spec/compose-go/v2 v2.14.0 h1:uaJeo5B3+OVlu+Rx2qLBcAdXPEUUzm5nQrRiGJafRAQ=
github.com/compose-spec/compose-go/v2 v2.14.0/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg=
github.com/compose-spec/compose-go/v2 v2.14.1-0.20260825154407-6c1c2d727681 h1:jqD2pgesJ/zRJwgaefxuSuT+/TGUlarjB93Zde40UtI=
github.com/compose-spec/compose-go/v2 v2.14.1-0.20260825154407-6c1c2d727681/go.mod h1:ZU6zlcweCZKyiB7BVfCizQT9XmkEIMFE+PRZydVcsZg=
github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6dyLYXQ=
github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw=
github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
Expand Down
5 changes: 5 additions & 0 deletions pkg/compose/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt
options.Services = project.ServiceNames()
}

// resolve the model once: optional depends_on references left dangling by
// profiles or service selection are pruned before anything (dependency
// graph, container labels) reads them
project = project.WithoutUnresolvedOptionalDependencies()

err := project.CheckContainerNameUnicity()
if err != nil {
return err
Expand Down
10 changes: 6 additions & 4 deletions pkg/compose/dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,11 @@ func (v *Vertex) GetChildren() []*Vertex {
return res
}

// NewGraph returns the dependency graph of the services
// NewGraph returns the dependency graph of the services. It never modifies
// the project: an optional (required: false) dependency on a service absent
// from the model simply contributes no edge; pruning such references from
// the model itself is the caller's explicit decision — see
// Project.WithoutUnresolvedOptionalDependencies.
func NewGraph(project *types.Project, initialStatus ServiceStatus) (*Graph, error) {
graph := &Graph{
lock: sync.RWMutex{},
Expand All @@ -258,13 +262,11 @@ func NewGraph(project *types.Project, initialStatus ServiceStatus) (*Graph, erro
graph.AddVertex(s.Name, s.Name, initialStatus)
}

for index, s := range project.Services {
for _, s := range project.Services {
for _, name := range s.GetDependencies() {
err := graph.AddEdge(s.Name, name)
if err != nil {
if !s.DependsOn[name].Required {
delete(s.DependsOn, name)
project.Services[index] = s
continue
}
if api.IsNotFoundError(err) {
Expand Down
34 changes: 34 additions & 0 deletions pkg/compose/dependencies_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,40 @@ func TestBuildGraphDependsOn(t *testing.T) {
}
}

// NewGraph must never rewrite the project it reads: pruning unresolved
// optional dependencies is an explicit, separate step. Building a graph any
// number of times leaves the model byte-identical, so what later readers
// (dependency waits, container labels) observe no longer depends on how many
// graphs were built before them.
func TestNewGraphDoesNotMutateProject(t *testing.T) {
project := &types.Project{
Services: types.Services{
"app": {
Name: "app",
DependsOn: types.DependsOnConfig{
"db": {Condition: types.ServiceConditionStarted, Required: true},
"debug": {Condition: types.ServiceConditionStarted, Required: false},
},
},
"db": {Name: "db"},
},
DisabledServices: types.Services{
"debug": {Name: "debug", Profiles: []string{"debug"}},
},
}

for range 2 {
graph, err := NewGraph(project, ServiceStopped)
assert.NilError(t, err)
// the unresolved optional dependency contributes no edge...
assert.Equal(t, len(graph.Vertices["app"].Children), 1)
// ...but stays in the model
assert.Equal(t, len(project.Services["app"].DependsOn), 2)
_, ok := project.Services["app"].DependsOn["debug"]
assert.Check(t, ok)
}
}

func isVertexEqual(a, b Vertex) bool {
childrenEquality := true
for c := range a.Children {
Expand Down
4 changes: 4 additions & 0 deletions pkg/compose/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ func (s *composeService) start(ctx context.Context, projectName string, options
return err
}
}
// resolve the model once: optional depends_on references left dangling by
// profiles or service selection are pruned before the dependency graph
// and the dependency waits read them
project = project.WithoutUnresolvedOptionalDependencies()

res, err := s.apiClient().ContainerList(ctx, client.ContainerListOptions{
Filters: projectFilter(project.Name).Add("label", oneOffFilter(false)),
Expand Down
Loading