diff --git a/go.mod b/go.mod index b6c7ba0e3f0..33bd2b88a70 100644 --- a/go.mod +++ b/go.mod @@ -174,3 +174,5 @@ require ( sigs.k8s.io/yaml v1.3.0 // indirect tags.cncf.io/container-device-interface v0.6.2 // indirect ) + +replace github.com/compose-spec/compose-go/v2 => github.com/ndeloof/compose-go/v2 v2.0.0-20231207163150-0eb507e0904b diff --git a/go.sum b/go.sum index 94b2e9197da..d3bed2363aa 100644 --- a/go.sum +++ b/go.sum @@ -132,8 +132,6 @@ github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+g github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 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.0.0-beta.1 h1:/A+2QMQVSsAmr9Gn5fm6YwaufjRZmWBnHYjr0oCyGiw= -github.com/compose-spec/compose-go/v2 v2.0.0-beta.1/go.mod h1:PWCgeD8cxiI/DmdpBM407CuLDrZ2W4xuS6/Z9jAi0YQ= github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= @@ -454,6 +452,8 @@ github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7P github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/ndeloof/compose-go/v2 v2.0.0-20231207163150-0eb507e0904b h1:d4DfO5xGakgBGrbiwkWYXy5StKDwUs7dswBXyoj7dk4= +github.com/ndeloof/compose-go/v2 v2.0.0-20231207163150-0eb507e0904b/go.mod h1:PWCgeD8cxiI/DmdpBM407CuLDrZ2W4xuS6/Z9jAi0YQ= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU= diff --git a/pkg/compose/build.go b/pkg/compose/build.go index 863cadbfcc2..bb23e6dd6be 100644 --- a/pkg/compose/build.go +++ b/pkg/compose/build.go @@ -23,6 +23,7 @@ import ( "os" "path/filepath" + "github.com/compose-spec/compose-go/v2/graph" "github.com/moby/buildkit/util/progress/progressui" "github.com/compose-spec/compose-go/v2/types" @@ -79,7 +80,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti imageIDs := map[string]string{} serviceToBeBuild := map[string]serviceToBuild{} - err = project.WithServices(options.Services, func(service types.ServiceConfig) error { + err = project.WithServices(options.Services, func(name string, service types.ServiceConfig) error { if service.Build == nil { return nil } @@ -88,7 +89,6 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti if localImagePresent && service.PullPolicy != types.PullPolicyBuild { return nil } - name := service.Name serviceToBeBuild[name] = serviceToBuild{name: name, service: service} return nil }, types.IgnoreDependencies) @@ -145,15 +145,14 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti } return -1 } - err = InDependencyOrder(ctx, project, func(ctx context.Context, name string) error { + err = graph.InDependencyOrder(ctx, project, func(ctx context.Context, name string, service types.ServiceConfig) error { if len(options.Services) > 0 && !utils.Contains(options.Services, name) { return nil } - serviceToBuild, ok := serviceToBeBuild[name] + _, ok := serviceToBeBuild[name] if !ok { return nil } - service := serviceToBuild.service if !buildkitEnabled { id, err := s.doBuildClassic(ctx, project, service, options) @@ -184,9 +183,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti builtDigests[getServiceIndex(name)] = digest return nil - }, func(traversal *graphTraversal) { - traversal.maxConcurrency = s.maxConcurrency - }) + }, graph.WithMaxConcurrency(s.maxConcurrency)) // enforce all build event get consumed if buildkitEnabled { diff --git a/pkg/compose/convergence.go b/pkg/compose/convergence.go index 4ba31ff09ac..8ef7d7d3726 100644 --- a/pkg/compose/convergence.go +++ b/pkg/compose/convergence.go @@ -26,6 +26,7 @@ import ( "sync" "time" + "github.com/compose-spec/compose-go/v2/graph" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -91,12 +92,7 @@ func newConvergence(services []string, state Containers, s *composeService) *con } func (c *convergence) apply(ctx context.Context, project *types.Project, options api.CreateOptions) error { - return InDependencyOrder(ctx, project, func(ctx context.Context, name string) error { - service, err := project.GetService(name) - if err != nil { - return err - } - + return graph.InDependencyOrder(ctx, project, func(ctx context.Context, name string, service types.ServiceConfig) error { return tracing.SpanWrapFunc("service/apply", tracing.ServiceOptions(service), func(ctx context.Context) error { strategy := options.RecreateDependencies if utils.StringContains(options.Services, name) { @@ -104,7 +100,7 @@ func (c *convergence) apply(ctx context.Context, project *types.Project, options } return c.ensureService(ctx, project, service, strategy, options.Inherit, options.Timeout) })(ctx) - }) + }, graph.WithMaxConcurrency(c.service.maxConcurrency)) } var mu sync.Mutex diff --git a/pkg/compose/dependencies.go b/pkg/compose/dependencies.go deleted file mode 100644 index 4a3fa4bd28c..00000000000 --- a/pkg/compose/dependencies.go +++ /dev/null @@ -1,480 +0,0 @@ -/* - Copyright 2020 Docker Compose CLI authors - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package compose - -import ( - "context" - "fmt" - "strings" - "sync" - - "github.com/compose-spec/compose-go/v2/types" - "github.com/docker/compose/v2/pkg/api" - "golang.org/x/sync/errgroup" - - "github.com/docker/compose/v2/pkg/utils" -) - -// ServiceStatus indicates the status of a service -type ServiceStatus int - -// Services status flags -const ( - ServiceStopped ServiceStatus = iota - ServiceStarted -) - -type graphTraversal struct { - mu sync.Mutex - seen map[string]struct{} - ignored map[string]struct{} - - extremityNodesFn func(*Graph) []*Vertex // leaves or roots - adjacentNodesFn func(*Vertex) []*Vertex // getParents or getChildren - filterAdjacentByStatusFn func(*Graph, string, ServiceStatus) []*Vertex // filterChildren or filterParents - targetServiceStatus ServiceStatus - adjacentServiceStatusToSkip ServiceStatus - - visitorFn func(context.Context, string) error - maxConcurrency int -} - -func upDirectionTraversal(visitorFn func(context.Context, string) error) *graphTraversal { - return &graphTraversal{ - extremityNodesFn: leaves, - adjacentNodesFn: getParents, - filterAdjacentByStatusFn: filterChildren, - adjacentServiceStatusToSkip: ServiceStopped, - targetServiceStatus: ServiceStarted, - visitorFn: visitorFn, - } -} - -func downDirectionTraversal(visitorFn func(context.Context, string) error) *graphTraversal { - return &graphTraversal{ - extremityNodesFn: roots, - adjacentNodesFn: getChildren, - filterAdjacentByStatusFn: filterParents, - adjacentServiceStatusToSkip: ServiceStarted, - targetServiceStatus: ServiceStopped, - visitorFn: visitorFn, - } -} - -// InDependencyOrder applies the function to the services of the project taking in account the dependency order -func InDependencyOrder(ctx context.Context, project *types.Project, fn func(context.Context, string) error, options ...func(*graphTraversal)) error { - graph, err := NewGraph(project, ServiceStopped) - if err != nil { - return err - } - t := upDirectionTraversal(fn) - for _, option := range options { - option(t) - } - return t.visit(ctx, graph) -} - -// InReverseDependencyOrder applies the function to the services of the project in reverse order of dependencies -func InReverseDependencyOrder(ctx context.Context, project *types.Project, fn func(context.Context, string) error, options ...func(*graphTraversal)) error { - graph, err := NewGraph(project, ServiceStarted) - if err != nil { - return err - } - t := downDirectionTraversal(fn) - for _, option := range options { - option(t) - } - return t.visit(ctx, graph) -} - -func WithRootNodesAndDown(nodes []string) func(*graphTraversal) { - return func(t *graphTraversal) { - if len(nodes) == 0 { - return - } - originalFn := t.extremityNodesFn - t.extremityNodesFn = func(graph *Graph) []*Vertex { - var want []string - for _, node := range nodes { - vertex := graph.Vertices[node] - want = append(want, vertex.Service) - for _, v := range getAncestors(vertex) { - want = append(want, v.Service) - } - } - - t.ignored = map[string]struct{}{} - for k := range graph.Vertices { - if !utils.Contains(want, k) { - t.ignored[k] = struct{}{} - } - } - - return originalFn(graph) - } - } -} - -func (t *graphTraversal) visit(ctx context.Context, g *Graph) error { - expect := len(g.Vertices) - if expect == 0 { - return nil - } - - eg, ctx := errgroup.WithContext(ctx) - if t.maxConcurrency > 0 { - eg.SetLimit(t.maxConcurrency + 1) - } - nodeCh := make(chan *Vertex, expect) - defer close(nodeCh) - // nodeCh need to allow n=expect writers while reader goroutine could have returner after ctx.Done - eg.Go(func() error { - for { - select { - case <-ctx.Done(): - return nil - case node := <-nodeCh: - expect-- - if expect == 0 { - return nil - } - t.run(ctx, g, eg, t.adjacentNodesFn(node), nodeCh) - } - } - }) - - nodes := t.extremityNodesFn(g) - t.run(ctx, g, eg, nodes, nodeCh) - - return eg.Wait() -} - -// Note: this could be `graph.walk` or whatever -func (t *graphTraversal) run(ctx context.Context, graph *Graph, eg *errgroup.Group, nodes []*Vertex, nodeCh chan *Vertex) { - for _, node := range nodes { - // Don't start this service yet if all of its children have - // not been started yet. - if len(t.filterAdjacentByStatusFn(graph, node.Key, t.adjacentServiceStatusToSkip)) != 0 { - continue - } - - node := node - if !t.consume(node.Key) { - // another worker already visited this node - continue - } - - eg.Go(func() error { - var err error - if _, ignore := t.ignored[node.Service]; !ignore { - err = t.visitorFn(ctx, node.Service) - } - if err == nil { - graph.UpdateStatus(node.Key, t.targetServiceStatus) - } - nodeCh <- node - return err - }) - } -} - -func (t *graphTraversal) consume(nodeKey string) bool { - t.mu.Lock() - defer t.mu.Unlock() - if t.seen == nil { - t.seen = make(map[string]struct{}) - } - if _, ok := t.seen[nodeKey]; ok { - return false - } - t.seen[nodeKey] = struct{}{} - return true -} - -// Graph represents project as service dependencies -type Graph struct { - Vertices map[string]*Vertex - lock sync.RWMutex -} - -// Vertex represents a service in the dependencies structure -type Vertex struct { - Key string - Service string - Status ServiceStatus - Children map[string]*Vertex - Parents map[string]*Vertex -} - -func getParents(v *Vertex) []*Vertex { - return v.GetParents() -} - -// GetParents returns a slice with the parent vertices of the a Vertex -func (v *Vertex) GetParents() []*Vertex { - var res []*Vertex - for _, p := range v.Parents { - res = append(res, p) - } - return res -} - -func getChildren(v *Vertex) []*Vertex { - return v.GetChildren() -} - -// getAncestors return all descendents for a vertex, might contain duplicates -func getAncestors(v *Vertex) []*Vertex { - var descendents []*Vertex - for _, parent := range v.GetParents() { - descendents = append(descendents, parent) - descendents = append(descendents, getAncestors(parent)...) - } - return descendents -} - -// GetChildren returns a slice with the child vertices of the a Vertex -func (v *Vertex) GetChildren() []*Vertex { - var res []*Vertex - for _, p := range v.Children { - res = append(res, p) - } - return res -} - -// NewGraph returns the dependency graph of the services -func NewGraph(project *types.Project, initialStatus ServiceStatus) (*Graph, error) { - graph := &Graph{ - lock: sync.RWMutex{}, - Vertices: map[string]*Vertex{}, - } - - for _, s := range project.Services { - graph.AddVertex(s.Name, s.Name, initialStatus) - } - - for index, 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) { - ds, err := project.GetDisabledService(name) - if err == nil { - return nil, fmt.Errorf("service %s is required by %s but is disabled. Can be enabled by profiles %s", name, s.Name, ds.Profiles) - } - } - return nil, err - } - } - } - - if b, err := graph.HasCycles(); b { - return nil, err - } - - return graph, nil -} - -// NewVertex is the constructor function for the Vertex -func NewVertex(key string, service string, initialStatus ServiceStatus) *Vertex { - return &Vertex{ - Key: key, - Service: service, - Status: initialStatus, - Parents: map[string]*Vertex{}, - Children: map[string]*Vertex{}, - } -} - -// AddVertex adds a vertex to the Graph -func (g *Graph) AddVertex(key string, service string, initialStatus ServiceStatus) { - g.lock.Lock() - defer g.lock.Unlock() - - v := NewVertex(key, service, initialStatus) - g.Vertices[key] = v -} - -// AddEdge adds a relationship of dependency between vertices `source` and `destination` -func (g *Graph) AddEdge(source string, destination string) error { - g.lock.Lock() - defer g.lock.Unlock() - - sourceVertex := g.Vertices[source] - destinationVertex := g.Vertices[destination] - - if sourceVertex == nil { - return fmt.Errorf("could not find %s: %w", source, api.ErrNotFound) - } - if destinationVertex == nil { - return fmt.Errorf("could not find %s: %w", destination, api.ErrNotFound) - } - - // If they are already connected - if _, ok := sourceVertex.Children[destination]; ok { - return nil - } - - sourceVertex.Children[destination] = destinationVertex - destinationVertex.Parents[source] = sourceVertex - - return nil -} - -func leaves(g *Graph) []*Vertex { - return g.Leaves() -} - -// Leaves returns the slice of leaves of the graph -func (g *Graph) Leaves() []*Vertex { - g.lock.Lock() - defer g.lock.Unlock() - - var res []*Vertex - for _, v := range g.Vertices { - if len(v.Children) == 0 { - res = append(res, v) - } - } - - return res -} - -func roots(g *Graph) []*Vertex { - return g.Roots() -} - -// Roots returns the slice of "Roots" of the graph -func (g *Graph) Roots() []*Vertex { - g.lock.Lock() - defer g.lock.Unlock() - - var res []*Vertex - for _, v := range g.Vertices { - if len(v.Parents) == 0 { - res = append(res, v) - } - } - return res -} - -// UpdateStatus updates the status of a certain vertex -func (g *Graph) UpdateStatus(key string, status ServiceStatus) { - g.lock.Lock() - defer g.lock.Unlock() - g.Vertices[key].Status = status -} - -func filterChildren(g *Graph, k string, s ServiceStatus) []*Vertex { - return g.FilterChildren(k, s) -} - -// FilterChildren returns children of a certain vertex that are in a certain status -func (g *Graph) FilterChildren(key string, status ServiceStatus) []*Vertex { - g.lock.Lock() - defer g.lock.Unlock() - - var res []*Vertex - vertex := g.Vertices[key] - - for _, child := range vertex.Children { - if child.Status == status { - res = append(res, child) - } - } - - return res -} - -func filterParents(g *Graph, k string, s ServiceStatus) []*Vertex { - return g.FilterParents(k, s) -} - -// FilterParents returns the parents of a certain vertex that are in a certain status -func (g *Graph) FilterParents(key string, status ServiceStatus) []*Vertex { - g.lock.Lock() - defer g.lock.Unlock() - - var res []*Vertex - vertex := g.Vertices[key] - - for _, parent := range vertex.Parents { - if parent.Status == status { - res = append(res, parent) - } - } - - return res -} - -// HasCycles detects cycles in the graph -func (g *Graph) HasCycles() (bool, error) { - discovered := []string{} - finished := []string{} - - for _, vertex := range g.Vertices { - path := []string{ - vertex.Key, - } - if !utils.StringContains(discovered, vertex.Key) && !utils.StringContains(finished, vertex.Key) { - var err error - discovered, finished, err = g.visit(vertex.Key, path, discovered, finished) - - if err != nil { - return true, err - } - } - } - - return false, nil -} - -func (g *Graph) visit(key string, path []string, discovered []string, finished []string) ([]string, []string, error) { - discovered = append(discovered, key) - - for _, v := range g.Vertices[key].Children { - path := append(path, v.Key) - if utils.StringContains(discovered, v.Key) { - return nil, nil, fmt.Errorf("cycle found: %s", strings.Join(path, " -> ")) - } - - if !utils.StringContains(finished, v.Key) { - if _, _, err := g.visit(v.Key, path, discovered, finished); err != nil { - return nil, nil, err - } - } - } - - discovered = remove(discovered, key) - finished = append(finished, key) - return discovered, finished, nil -} - -func remove(slice []string, item string) []string { - var s []string - for _, i := range slice { - if i != item { - s = append(s, i) - } - } - return s -} diff --git a/pkg/compose/dependencies_test.go b/pkg/compose/dependencies_test.go deleted file mode 100644 index b54702fb39b..00000000000 --- a/pkg/compose/dependencies_test.go +++ /dev/null @@ -1,387 +0,0 @@ -/* - Copyright 2020 Docker Compose CLI authors - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package compose - -import ( - "context" - "fmt" - "sort" - "sync" - "testing" - - "github.com/compose-spec/compose-go/v2/types" - "github.com/docker/compose/v2/pkg/utils" - testify "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gotest.tools/v3/assert" -) - -func createTestProject() *types.Project { - return &types.Project{ - Services: types.Services{ - "test1": { - Name: "test1", - DependsOn: map[string]types.ServiceDependency{ - "test2": {}, - }, - }, - "test2": { - Name: "test2", - DependsOn: map[string]types.ServiceDependency{ - "test3": {}, - }, - }, - "test3": { - Name: "test3", - }, - }, - } -} - -func TestTraversalWithMultipleParents(t *testing.T) { - dependent := types.ServiceConfig{ - Name: "dependent", - DependsOn: make(types.DependsOnConfig), - } - - project := types.Project{ - Services: types.Services{"dependent": dependent}, - } - - for i := 1; i <= 100; i++ { - name := fmt.Sprintf("svc_%d", i) - dependent.DependsOn[name] = types.ServiceDependency{} - - svc := types.ServiceConfig{Name: name} - project.Services[name] = svc - } - - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - - svc := make(chan string, 10) - seen := make(map[string]int) - done := make(chan struct{}) - go func() { - for service := range svc { - seen[service]++ - } - done <- struct{}{} - }() - - err := InDependencyOrder(ctx, &project, func(ctx context.Context, service string) error { - svc <- service - return nil - }) - require.NoError(t, err, "Error during iteration") - close(svc) - <-done - - testify.Len(t, seen, 101) - for svc, count := range seen { - assert.Equal(t, 1, count, "Service: %s", svc) - } -} - -func TestInDependencyUpCommandOrder(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - - var order []string - err := InDependencyOrder(ctx, createTestProject(), func(ctx context.Context, service string) error { - order = append(order, service) - return nil - }) - require.NoError(t, err, "Error during iteration") - require.Equal(t, []string{"test3", "test2", "test1"}, order) -} - -func TestInDependencyReverseDownCommandOrder(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - - var order []string - err := InReverseDependencyOrder(ctx, createTestProject(), func(ctx context.Context, service string) error { - order = append(order, service) - return nil - }) - require.NoError(t, err, "Error during iteration") - require.Equal(t, []string{"test1", "test2", "test3"}, order) -} - -func TestBuildGraph(t *testing.T) { - testCases := []struct { - desc string - services types.Services - expectedVertices map[string]*Vertex - }{ - { - desc: "builds graph with single service", - services: types.Services{ - "test": { - Name: "test", - DependsOn: types.DependsOnConfig{}, - }, - }, - expectedVertices: map[string]*Vertex{ - "test": { - Key: "test", - Service: "test", - Status: ServiceStopped, - Children: map[string]*Vertex{}, - Parents: map[string]*Vertex{}, - }, - }, - }, - { - desc: "builds graph with two separate services", - services: types.Services{ - "test": { - Name: "test", - DependsOn: types.DependsOnConfig{}, - }, - "another": { - Name: "another", - DependsOn: types.DependsOnConfig{}, - }, - }, - expectedVertices: map[string]*Vertex{ - "test": { - Key: "test", - Service: "test", - Status: ServiceStopped, - Children: map[string]*Vertex{}, - Parents: map[string]*Vertex{}, - }, - "another": { - Key: "another", - Service: "another", - Status: ServiceStopped, - Children: map[string]*Vertex{}, - Parents: map[string]*Vertex{}, - }, - }, - }, - { - desc: "builds graph with a service and a dependency", - services: types.Services{ - "test": { - Name: "test", - DependsOn: types.DependsOnConfig{ - "another": types.ServiceDependency{}, - }, - }, - "another": { - Name: "another", - DependsOn: types.DependsOnConfig{}, - }, - }, - expectedVertices: map[string]*Vertex{ - "test": { - Key: "test", - Service: "test", - Status: ServiceStopped, - Children: map[string]*Vertex{ - "another": {}, - }, - Parents: map[string]*Vertex{}, - }, - "another": { - Key: "another", - Service: "another", - Status: ServiceStopped, - Children: map[string]*Vertex{}, - Parents: map[string]*Vertex{ - "test": {}, - }, - }, - }, - }, - { - desc: "builds graph with multiple dependency levels", - services: types.Services{ - "test": { - Name: "test", - DependsOn: types.DependsOnConfig{ - "another": types.ServiceDependency{}, - }, - }, - "another": { - Name: "another", - DependsOn: types.DependsOnConfig{ - "another_dep": types.ServiceDependency{}, - }, - }, - "another_dep": { - Name: "another_dep", - DependsOn: types.DependsOnConfig{}, - }, - }, - expectedVertices: map[string]*Vertex{ - "test": { - Key: "test", - Service: "test", - Status: ServiceStopped, - Children: map[string]*Vertex{ - "another": {}, - }, - Parents: map[string]*Vertex{}, - }, - "another": { - Key: "another", - Service: "another", - Status: ServiceStopped, - Children: map[string]*Vertex{ - "another_dep": {}, - }, - Parents: map[string]*Vertex{ - "test": {}, - }, - }, - "another_dep": { - Key: "another_dep", - Service: "another_dep", - Status: ServiceStopped, - Children: map[string]*Vertex{}, - Parents: map[string]*Vertex{ - "another": {}, - }, - }, - }, - }, - } - for _, tC := range testCases { - t.Run(tC.desc, func(t *testing.T) { - project := types.Project{ - Services: tC.services, - } - - graph, err := NewGraph(&project, ServiceStopped) - assert.NilError(t, err, fmt.Sprintf("failed to build graph for: %s", tC.desc)) - - for k, vertex := range graph.Vertices { - expected, ok := tC.expectedVertices[k] - assert.Equal(t, true, ok) - assert.Equal(t, true, isVertexEqual(*expected, *vertex)) - } - }) - } -} - -func isVertexEqual(a, b Vertex) bool { - childrenEquality := true - for c := range a.Children { - if _, ok := b.Children[c]; !ok { - childrenEquality = false - } - } - parentEquality := true - for p := range a.Parents { - if _, ok := b.Parents[p]; !ok { - parentEquality = false - } - } - return a.Key == b.Key && - a.Service == b.Service && - childrenEquality && - parentEquality -} - -func TestWith_RootNodesAndUp(t *testing.T) { - graph := &Graph{ - lock: sync.RWMutex{}, - Vertices: map[string]*Vertex{}, - } - - /** graph topology: - A B - / \ / \ - G C E - \ / - D - | - F - */ - - graph.AddVertex("A", "A", 0) - graph.AddVertex("B", "B", 0) - graph.AddVertex("C", "C", 0) - graph.AddVertex("D", "D", 0) - graph.AddVertex("E", "E", 0) - graph.AddVertex("F", "F", 0) - graph.AddVertex("G", "G", 0) - - _ = graph.AddEdge("C", "A") - _ = graph.AddEdge("C", "B") - _ = graph.AddEdge("E", "B") - _ = graph.AddEdge("D", "C") - _ = graph.AddEdge("D", "E") - _ = graph.AddEdge("F", "D") - _ = graph.AddEdge("G", "A") - - tests := []struct { - name string - nodes []string - want []string - }{ - { - name: "whole graph", - nodes: []string{"A", "B"}, - want: []string{"A", "B", "C", "D", "E", "F", "G"}, - }, - { - name: "only leaves", - nodes: []string{"F", "G"}, - want: []string{"F", "G"}, - }, - { - name: "simple dependent", - nodes: []string{"D"}, - want: []string{"D", "F"}, - }, - { - name: "diamond dependents", - nodes: []string{"B"}, - want: []string{"B", "C", "D", "E", "F"}, - }, - { - name: "partial graph", - nodes: []string{"A"}, - want: []string{"A", "C", "D", "F", "G"}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mx := sync.Mutex{} - expected := utils.Set[string]{} - expected.AddAll("C", "G", "D", "F") - var visited []string - - gt := downDirectionTraversal(func(ctx context.Context, s string) error { - mx.Lock() - defer mx.Unlock() - visited = append(visited, s) - return nil - }) - WithRootNodesAndDown(tt.nodes)(gt) - err := gt.visit(context.TODO(), graph) - assert.NilError(t, err) - sort.Strings(visited) - assert.DeepEqual(t, tt.want, visited) - }) - } -} diff --git a/pkg/compose/down.go b/pkg/compose/down.go index 009dcf8f56d..63f83d6861b 100644 --- a/pkg/compose/down.go +++ b/pkg/compose/down.go @@ -22,6 +22,7 @@ import ( "strings" "time" + "github.com/compose-spec/compose-go/v2/graph" "github.com/docker/compose/v2/pkg/utils" "github.com/compose-spec/compose-go/v2/types" @@ -74,11 +75,11 @@ func (s *composeService) down(ctx context.Context, projectName string, options a resourceToRemove = true } - err = InReverseDependencyOrder(ctx, project, func(c context.Context, service string) error { - serviceContainers := containers.filter(isService(service)) + err = graph.InDependencyOrder(ctx, project, func(c context.Context, name string, service types.ServiceConfig) error { + serviceContainers := containers.filter(isService(name)) err := s.removeContainers(ctx, w, serviceContainers, options.Timeout, options.Volumes) return err - }, WithRootNodesAndDown(options.Services)) + }, graph.WithRootNodesAndDown(options.Services)) if err != nil { return err } diff --git a/pkg/compose/restart.go b/pkg/compose/restart.go index 0a93813ed2f..716fb06fdb0 100644 --- a/pkg/compose/restart.go +++ b/pkg/compose/restart.go @@ -20,6 +20,7 @@ import ( "context" "strings" + "github.com/compose-spec/compose-go/v2/graph" "github.com/compose-spec/compose-go/v2/types" "github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/progress" @@ -73,9 +74,9 @@ func (s *composeService) restart(ctx context.Context, projectName string, option } w := progress.ContextWriter(ctx) - return InDependencyOrder(ctx, project, func(c context.Context, service string) error { + return graph.InDependencyOrder(ctx, project, func(c context.Context, name string, _ types.ServiceConfig) error { eg, ctx := errgroup.WithContext(ctx) - for _, container := range containers.filter(isService(service)) { + for _, container := range containers.filter(isService(name)) { container := container eg.Go(func() error { eventName := getContainerProgressName(container) @@ -89,5 +90,5 @@ func (s *composeService) restart(ctx context.Context, projectName string, option }) } return eg.Wait() - }) + }, graph.WithMaxConcurrency(s.maxConcurrency)) } diff --git a/pkg/compose/start.go b/pkg/compose/start.go index dd35a2e94cc..18afc84819f 100644 --- a/pkg/compose/start.go +++ b/pkg/compose/start.go @@ -23,6 +23,7 @@ import ( "strings" "time" + "github.com/compose-spec/compose-go/v2/graph" containerType "github.com/docker/docker/api/types/container" "github.com/docker/docker/errdefs" @@ -123,14 +124,9 @@ func (s *composeService) start(ctx context.Context, projectName string, options return err } - err = InDependencyOrder(ctx, project, func(c context.Context, name string) error { - service, err := project.GetService(name) - if err != nil { - return err - } - + err = graph.InDependencyOrder(ctx, project, func(c context.Context, name string, service types.ServiceConfig) error { return s.startService(ctx, project, service, containers) - }) + }, graph.WithMaxConcurrency(s.maxConcurrency)) if err != nil { return err } diff --git a/pkg/compose/stop.go b/pkg/compose/stop.go index a7f6d68db55..7c9a2c7776e 100644 --- a/pkg/compose/stop.go +++ b/pkg/compose/stop.go @@ -20,6 +20,8 @@ import ( "context" "strings" + "github.com/compose-spec/compose-go/v2/graph" + "github.com/compose-spec/compose-go/v2/types" "github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/utils" @@ -50,10 +52,10 @@ func (s *composeService) stop(ctx context.Context, projectName string, options a } w := progress.ContextWriter(ctx) - return InReverseDependencyOrder(ctx, project, func(c context.Context, service string) error { - if !utils.StringContains(options.Services, service) { + return graph.InDependencyOrder(ctx, project, func(c context.Context, name string, service types.ServiceConfig) error { + if !utils.StringContains(options.Services, name) { return nil } - return s.stopContainers(ctx, w, containers.filter(isService(service)).filter(isNotOneOff), options.Timeout) - }) + return s.stopContainers(ctx, w, containers.filter(isService(name)).filter(isNotOneOff), options.Timeout) + }, graph.InReverseOrder, graph.WithMaxConcurrency(s.maxConcurrency)) }