Skip to content
Merged
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
6 changes: 3 additions & 3 deletions pkg/compose/events.go → internal/inspect/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
limitations under the License.
*/

package compose
package inspect

import (
"context"
Expand All @@ -27,9 +27,9 @@ import (
"github.com/docker/compose/v5/pkg/api"
)

func (s *composeService) Events(ctx context.Context, projectName string, options api.EventsOptions) error {
func (s *Service) Events(ctx context.Context, projectName string, options api.EventsOptions) error {
projectName = strings.ToLower(projectName)
res := s.apiClient().Events(ctx, client.EventsListOptions{
res := s.backend.APIClient().Events(ctx, client.EventsListOptions{
Filters: projectFilter(projectName),
Since: options.Since,
Until: options.Until,
Expand Down
87 changes: 87 additions & 0 deletions internal/inspect/inspect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
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 inspect implements the read-only operations that report on a
// project and its resources: ps, ls, top, port, volumes, events and viz.
package inspect

import (
"context"
"slices"

"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"golang.org/x/sync/errgroup"

"github.com/docker/compose/v5/pkg/api"
)

// OneOff tells whether the containers of `compose run` are part of a listing.
type OneOff int

const (
// OneOffInclude lists the one-off containers along with the services' ones.
OneOffInclude OneOff = iota
// OneOffExclude leaves the one-off containers out.
OneOffExclude
)

// Backend is what the operations need from the engine they report on.
type Backend interface {
// APIClient talks to the Docker daemon.
APIClient() client.APIClient
// MaxConcurrency bounds the daemon calls made in parallel, 0 meaning no bound.
MaxConcurrency() int
// Containers lists the containers of a project, narrowed to services when given.
Containers(ctx context.Context, projectName string, oneOff OneOff, all bool, services ...string) ([]container.Summary, error)
// SpecifiedContainer returns the container of a service, the replica with
// the given index when it is above zero.
SpecifiedContainer(ctx context.Context, projectName string, oneOff OneOff, all bool, serviceName string, index int) (container.Summary, error)
}

// Service runs the inspection operations on a Backend.
type Service struct {
backend Backend
}

// New creates a Service reporting on the given Backend.
func New(backend Backend) *Service {
return &Service{backend: backend}
}

func projectFilter(projectName string) client.Filters {
return make(client.Filters).Add("label", api.ProjectLabel+"="+projectName)
}

// inServices keeps the containers of the given services.
func inServices(containers []container.Summary, services []string) []container.Summary {
var filtered []container.Summary
for _, ctr := range containers {
if slices.Contains(services, ctr.Labels[api.ServiceLabel]) {
filtered = append(filtered, ctr)
}
}
return filtered
}

// limitedGroup returns an errgroup running at most maxConcurrency goroutines at once.
func limitedGroup(ctx context.Context, maxConcurrency int) (*errgroup.Group, context.Context) {
eg, ctx := errgroup.WithContext(ctx)
if maxConcurrency > 0 {
eg.SetLimit(maxConcurrency)
}
return eg, ctx
}
108 changes: 108 additions & 0 deletions internal/inspect/inspect_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
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 inspect

import (
"context"
"testing"

"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"go.uber.org/mock/gomock"
"gotest.tools/v3/assert"

"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/mocks"
)

// fakeBackend serves canned containers and records how it was asked for them.
type fakeBackend struct {
apiClient client.APIClient
listed []container.Summary

oneOff OneOff
all bool
services []string
index int
}

func (f *fakeBackend) APIClient() client.APIClient { return f.apiClient }
func (f *fakeBackend) MaxConcurrency() int { return 2 }

func (f *fakeBackend) Containers(_ context.Context, _ string, oneOff OneOff, all bool, services ...string) ([]container.Summary, error) {
f.oneOff, f.all, f.services = oneOff, all, services
return f.listed, nil
}

func (f *fakeBackend) SpecifiedContainer(_ context.Context, _ string, oneOff OneOff, all bool, serviceName string, index int) (container.Summary, error) {
f.oneOff, f.all, f.services, f.index = oneOff, all, []string{serviceName}, index
return f.listed[0], nil
}

func summary(service, id string) container.Summary {
return container.Summary{
ID: id,
Names: []string{"/" + id},
Labels: map[string]string{api.ServiceLabel: service},
}
}

func TestPsOneOffContainersOnlyWithAll(t *testing.T) {
for _, all := range []bool{false, true} {
ctrl := gomock.NewController(t)
apiClient := mocks.NewMockAPIClient(ctrl)
apiClient.EXPECT().ContainerInspect(gomock.Any(), "web1", gomock.Any()).Return(client.ContainerInspectResult{}, nil)
backend := &fakeBackend{apiClient: apiClient, listed: []container.Summary{summary("web", "web1")}}

got, err := New(backend).Ps(t.Context(), "proj", api.PsOptions{All: all})

assert.NilError(t, err)
assert.Equal(t, len(got), 1)
assert.Equal(t, got[0].Service, "web")
assert.Equal(t, backend.all, all)
if all {
assert.Equal(t, backend.oneOff, OneOffInclude)
} else {
assert.Equal(t, backend.oneOff, OneOffExclude)
}
}
}

func TestPsKeepsTheRequestedServices(t *testing.T) {
ctrl := gomock.NewController(t)
apiClient := mocks.NewMockAPIClient(ctrl)
apiClient.EXPECT().ContainerInspect(gomock.Any(), "db1", gomock.Any()).Return(client.ContainerInspectResult{}, nil)
backend := &fakeBackend{apiClient: apiClient, listed: []container.Summary{summary("web", "web1"), summary("db", "db1")}}

got, err := New(backend).Ps(t.Context(), "proj", api.PsOptions{Services: []string{"db"}})

assert.NilError(t, err)
assert.Equal(t, len(got), 1)
assert.Equal(t, got[0].ID, "db1")
}

func TestPortsAsksForTheReplica(t *testing.T) {
backend := &fakeBackend{listed: []container.Summary{summary("web", "web2")}}

_, err := New(backend).Ports(t.Context(), "proj", "web", 0, api.PortOptions{Index: 2})

assert.NilError(t, err)
assert.DeepEqual(t, backend.services, []string{"web"})
assert.Equal(t, backend.index, 2)
assert.Equal(t, backend.oneOff, OneOffInclude)
assert.Equal(t, backend.all, false)
}
6 changes: 3 additions & 3 deletions pkg/compose/ls.go → internal/inspect/ls.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
limitations under the License.
*/

package compose
package inspect

import (
"context"
Expand All @@ -30,8 +30,8 @@ import (
"github.com/docker/compose/v5/pkg/api"
)

func (s *composeService) List(ctx context.Context, options api.ListOptions) ([]api.Stack, error) {
list, err := s.apiClient().ContainerList(ctx, client.ContainerListOptions{
func (s *Service) List(ctx context.Context, options api.ListOptions) ([]api.Stack, error) {
list, err := s.backend.APIClient().ContainerList(ctx, client.ContainerListOptions{
Filters: make(client.Filters).Add("label", api.ProjectLabel).Add("label", api.ConfigHashLabel),
All: options.All,
})
Expand Down
2 changes: 1 addition & 1 deletion pkg/compose/ls_test.go → internal/inspect/ls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
limitations under the License.
*/

package compose
package inspect

import (
"fmt"
Expand Down
6 changes: 3 additions & 3 deletions pkg/compose/port.go → internal/inspect/port.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
limitations under the License.
*/

package compose
package inspect

import (
"context"
Expand All @@ -26,9 +26,9 @@ import (
"github.com/docker/compose/v5/pkg/api"
)

func (s *composeService) Ports(ctx context.Context, projectName string, serviceName string, port uint16, options api.PortOptions) (api.PortPublishers, error) {
func (s *Service) Ports(ctx context.Context, projectName string, serviceName string, port uint16, options api.PortOptions) (api.PortPublishers, error) {
projectName = strings.ToLower(projectName)
ctr, err := s.getSpecifiedContainer(ctx, projectName, oneOffInclude, false, serviceName, options.Index)
ctr, err := s.backend.SpecifiedContainer(ctx, projectName, OneOffInclude, false, serviceName, options.Index)
if err != nil {
return nil, err
}
Expand Down
61 changes: 61 additions & 0 deletions internal/inspect/port_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
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 inspect

import (
"testing"

"github.com/moby/moby/api/types/container"
"gotest.tools/v3/assert"
)

// TestContainerPublishersPreservesTieOrder guards against containerPublishers
// reordering same-PrivatePort entries (e.g. a dual-stack publish exposing the
// same target port on both an IPv4 and an IPv6 host address): Ports() picks
// publishers[0] as *the* answer for a single-port lookup, so a sort that
// merely orders by PrivatePort must not additionally scramble ties.
//
// sort.Slice's pdqsort falls back to a (stable) insertion sort for slices of
// <=12 elements, so a small fixture can't tell a stable sort from an unstable
// one; this uses enough tied groups to force the quicksort partitioning path.
func TestContainerPublishersPreservesTieOrder(t *testing.T) {
var ports []container.PortSummary
privatePorts := []int{80, 53, 443}
for _, pp := range privatePorts {
for i := range 5 {
ports = append(ports, container.PortSummary{
PrivatePort: uint16(pp),
PublicPort: uint16(i), // tags each tied entry with its input position
Type: "tcp",
})
}
}
ports = append(ports, container.PortSummary{PrivatePort: 22, PublicPort: 0, Type: "tcp"})

ctr := container.Summary{ID: "123", Ports: ports}

got := containerPublishers(ctr)

lastPosByTargetPort := map[int]int{}
for _, p := range got {
pos := p.PublishedPort
if prev, ok := lastPosByTargetPort[p.TargetPort]; ok {
assert.Assert(t, pos > prev, "tied entries for private port %d out of input order: got position %d after %d", p.TargetPort, pos, prev)
}
lastPosByTargetPort[p.TargetPort] = pos
}
}
21 changes: 11 additions & 10 deletions pkg/compose/ps.go → internal/inspect/ps.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
limitations under the License.
*/

package compose
package inspect

import (
"context"
Expand All @@ -24,25 +24,26 @@ import (
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"

"github.com/docker/compose/v5/internal/naming"
"github.com/docker/compose/v5/pkg/api"
)

func (s *composeService) Ps(ctx context.Context, projectName string, options api.PsOptions) ([]api.ContainerSummary, error) {
func (s *Service) Ps(ctx context.Context, projectName string, options api.PsOptions) ([]api.ContainerSummary, error) {
projectName = strings.ToLower(projectName)
oneOff := oneOffExclude
oneOff := OneOffExclude
if options.All {
oneOff = oneOffInclude
oneOff = OneOffInclude
}
containers, err := s.getContainers(ctx, projectName, oneOff, options.All, options.Services...)
containers, err := s.backend.Containers(ctx, projectName, oneOff, options.All, options.Services...)
if err != nil {
return nil, err
}

if len(options.Services) != 0 {
containers = containers.filter(isService(options.Services...))
containers = inServices(containers, options.Services)
}
summary := make([]api.ContainerSummary, len(containers))
eg, ctx := newLimitedErrgroup(ctx, s.maxConcurrency)
eg, ctx := limitedGroup(ctx, s.backend.MaxConcurrency())
for i, ctr := range containers {
eg.Go(func() error {
var err error
Expand All @@ -55,16 +56,16 @@ func (s *composeService) Ps(ctx context.Context, projectName string, options api

// containerSummary builds the api summary for a container, inspecting it to
// retrieve its health and exit code
func (s *composeService) containerSummary(ctx context.Context, ctr container.Summary) (api.ContainerSummary, error) {
inspect, err := s.apiClient().ContainerInspect(ctx, ctr.ID, client.ContainerInspectOptions{})
func (s *Service) containerSummary(ctx context.Context, ctr container.Summary) (api.ContainerSummary, error) {
inspect, err := s.backend.APIClient().ContainerInspect(ctx, ctr.ID, client.ContainerInspectOptions{})
if err != nil {
return api.ContainerSummary{}, err
}
health, exitCode := containerHealthAndExitCode(inspect)
mounts, localVolumes := containerMounts(ctr)
return api.ContainerSummary{
ID: ctr.ID,
Name: getCanonicalContainerName(ctr),
Name: naming.CanonicalContainerName(ctr),
Names: ctr.Names,
Image: ctr.Image,
Project: ctr.Labels[api.ProjectLabel],
Expand Down
Loading
Loading