mirror of https://github.com/docker/cli.git
Merge pull request #2157 from thaJeztah/servicestatus
Services: use ServiceStatus on API v1.41 and up
This commit is contained in:
commit
b3cde356f6
|
@ -6,6 +6,7 @@ import (
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/swarm"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
|
|
||||||
// Import builders to get the builder function as package function
|
// Import builders to get the builder function as package function
|
||||||
. "github.com/docker/cli/internal/test/builders"
|
. "github.com/docker/cli/internal/test/builders"
|
||||||
)
|
)
|
||||||
|
@ -18,9 +19,13 @@ type fakeClient struct {
|
||||||
taskListFunc func(context.Context, types.TaskListOptions) ([]swarm.Task, error)
|
taskListFunc func(context.Context, types.TaskListOptions) ([]swarm.Task, error)
|
||||||
infoFunc func(ctx context.Context) (types.Info, error)
|
infoFunc func(ctx context.Context) (types.Info, error)
|
||||||
networkInspectFunc func(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error)
|
networkInspectFunc func(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error)
|
||||||
|
nodeListFunc func(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeClient) NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) {
|
func (f *fakeClient) NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) {
|
||||||
|
if f.nodeListFunc != nil {
|
||||||
|
return f.nodeListFunc(ctx, options)
|
||||||
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,9 +74,6 @@ func (f *fakeClient) NetworkInspect(ctx context.Context, networkID string, optio
|
||||||
return types.NetworkResource{}, nil
|
return types.NetworkResource{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newService(id string, name string) swarm.Service {
|
func newService(id string, name string, opts ...func(*swarm.Service)) swarm.Service {
|
||||||
return swarm.Service{
|
return *Service(append(opts, ServiceID(id), ServiceName(name))...)
|
||||||
ID: id,
|
|
||||||
Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: name}},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/docker/docker/pkg/stringid"
|
"github.com/docker/docker/pkg/stringid"
|
||||||
units "github.com/docker/go-units"
|
units "github.com/docker/go-units"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"vbom.ml/util/sortorder"
|
||||||
)
|
)
|
||||||
|
|
||||||
const serviceInspectPrettyTemplate formatter.Format = `
|
const serviceInspectPrettyTemplate formatter.Format = `
|
||||||
|
@ -520,17 +521,14 @@ func NewListFormat(source string, quiet bool) formatter.Format {
|
||||||
return formatter.Format(source)
|
return formatter.Format(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListInfo stores the information about mode and replicas to be used by template
|
|
||||||
type ListInfo struct {
|
|
||||||
Mode string
|
|
||||||
Replicas string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListFormatWrite writes the context
|
// ListFormatWrite writes the context
|
||||||
func ListFormatWrite(ctx formatter.Context, services []swarm.Service, info map[string]ListInfo) error {
|
func ListFormatWrite(ctx formatter.Context, services []swarm.Service) error {
|
||||||
render := func(format func(subContext formatter.SubContext) error) error {
|
render := func(format func(subContext formatter.SubContext) error) error {
|
||||||
|
sort.Slice(services, func(i, j int) bool {
|
||||||
|
return sortorder.NaturalLess(services[i].Spec.Name, services[j].Spec.Name)
|
||||||
|
})
|
||||||
for _, service := range services {
|
for _, service := range services {
|
||||||
serviceCtx := &serviceContext{service: service, mode: info[service.ID].Mode, replicas: info[service.ID].Replicas}
|
serviceCtx := &serviceContext{service: service}
|
||||||
if err := format(serviceCtx); err != nil {
|
if err := format(serviceCtx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -551,9 +549,7 @@ func ListFormatWrite(ctx formatter.Context, services []swarm.Service, info map[s
|
||||||
|
|
||||||
type serviceContext struct {
|
type serviceContext struct {
|
||||||
formatter.HeaderContext
|
formatter.HeaderContext
|
||||||
service swarm.Service
|
service swarm.Service
|
||||||
mode string
|
|
||||||
replicas string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *serviceContext) MarshalJSON() ([]byte, error) {
|
func (c *serviceContext) MarshalJSON() ([]byte, error) {
|
||||||
|
@ -569,11 +565,35 @@ func (c *serviceContext) Name() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *serviceContext) Mode() string {
|
func (c *serviceContext) Mode() string {
|
||||||
return c.mode
|
switch {
|
||||||
|
case c.service.Spec.Mode.Global != nil:
|
||||||
|
return "global"
|
||||||
|
case c.service.Spec.Mode.Replicated != nil:
|
||||||
|
return "replicated"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *serviceContext) Replicas() string {
|
func (c *serviceContext) Replicas() string {
|
||||||
return c.replicas
|
s := &c.service
|
||||||
|
|
||||||
|
var running, desired uint64
|
||||||
|
if s.ServiceStatus != nil {
|
||||||
|
running = c.service.ServiceStatus.RunningTasks
|
||||||
|
desired = c.service.ServiceStatus.DesiredTasks
|
||||||
|
}
|
||||||
|
if r := c.maxReplicas(); r > 0 {
|
||||||
|
return fmt.Sprintf("%d/%d (max %d per node)", running, desired, r)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d/%d", running, desired)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *serviceContext) maxReplicas() uint64 {
|
||||||
|
if c.Mode() != "replicated" || c.service.Spec.TaskTemplate.Placement == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return c.service.Spec.TaskTemplate.Placement.MaxReplicas
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *serviceContext) Image() string {
|
func (c *serviceContext) Image() string {
|
||||||
|
|
|
@ -33,29 +33,37 @@ func TestServiceContextWrite(t *testing.T) {
|
||||||
// Table format
|
// Table format
|
||||||
{
|
{
|
||||||
formatter.Context{Format: NewListFormat("table", false)},
|
formatter.Context{Format: NewListFormat("table", false)},
|
||||||
`ID NAME MODE REPLICAS IMAGE PORTS
|
`ID NAME MODE REPLICAS IMAGE PORTS
|
||||||
id_baz baz global 2/4 *:80->8080/tcp
|
02_bar bar replicated 2/4 *:80->8090/udp
|
||||||
id_bar bar replicated 2/4 *:80->8080/tcp
|
01_baz baz global 1/3 *:80->8080/tcp
|
||||||
|
04_qux2 qux2 replicated 3/3 (max 2 per node)
|
||||||
|
03_qux10 qux10 replicated 2/3 (max 1 per node)
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
formatter.Context{Format: NewListFormat("table", true)},
|
formatter.Context{Format: NewListFormat("table", true)},
|
||||||
`id_baz
|
`02_bar
|
||||||
id_bar
|
01_baz
|
||||||
|
04_qux2
|
||||||
|
03_qux10
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
formatter.Context{Format: NewListFormat("table {{.Name}}", false)},
|
formatter.Context{Format: NewListFormat("table {{.Name}}\t{{.Mode}}", false)},
|
||||||
`NAME
|
`NAME MODE
|
||||||
baz
|
bar replicated
|
||||||
bar
|
baz global
|
||||||
|
qux2 replicated
|
||||||
|
qux10 replicated
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
formatter.Context{Format: NewListFormat("table {{.Name}}", true)},
|
formatter.Context{Format: NewListFormat("table {{.Name}}", true)},
|
||||||
`NAME
|
`NAME
|
||||||
baz
|
|
||||||
bar
|
bar
|
||||||
|
baz
|
||||||
|
qux2
|
||||||
|
qux10
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
// Raw Format
|
// Raw Format
|
||||||
|
@ -65,15 +73,19 @@ bar
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
formatter.Context{Format: NewListFormat("raw", true)},
|
formatter.Context{Format: NewListFormat("raw", true)},
|
||||||
`id: id_baz
|
`id: 02_bar
|
||||||
id: id_bar
|
id: 01_baz
|
||||||
|
id: 04_qux2
|
||||||
|
id: 03_qux10
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
// Custom Format
|
// Custom Format
|
||||||
{
|
{
|
||||||
formatter.Context{Format: NewListFormat("{{.Name}}", false)},
|
formatter.Context{Format: NewListFormat("{{.Name}}", false)},
|
||||||
`baz
|
`bar
|
||||||
bar
|
baz
|
||||||
|
qux2
|
||||||
|
qux10
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -81,9 +93,12 @@ bar
|
||||||
for _, testcase := range cases {
|
for _, testcase := range cases {
|
||||||
services := []swarm.Service{
|
services := []swarm.Service{
|
||||||
{
|
{
|
||||||
ID: "id_baz",
|
ID: "01_baz",
|
||||||
Spec: swarm.ServiceSpec{
|
Spec: swarm.ServiceSpec{
|
||||||
Annotations: swarm.Annotations{Name: "baz"},
|
Annotations: swarm.Annotations{Name: "baz"},
|
||||||
|
Mode: swarm.ServiceMode{
|
||||||
|
Global: &swarm.GlobalService{},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Endpoint: swarm.Endpoint{
|
Endpoint: swarm.Endpoint{
|
||||||
Ports: []swarm.PortConfig{
|
Ports: []swarm.PortConfig{
|
||||||
|
@ -95,37 +110,70 @@ bar
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
ServiceStatus: &swarm.ServiceStatus{
|
||||||
|
RunningTasks: 1,
|
||||||
|
DesiredTasks: 3,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "id_bar",
|
ID: "02_bar",
|
||||||
Spec: swarm.ServiceSpec{
|
Spec: swarm.ServiceSpec{
|
||||||
Annotations: swarm.Annotations{Name: "bar"},
|
Annotations: swarm.Annotations{Name: "bar"},
|
||||||
|
Mode: swarm.ServiceMode{
|
||||||
|
Replicated: &swarm.ReplicatedService{},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Endpoint: swarm.Endpoint{
|
Endpoint: swarm.Endpoint{
|
||||||
Ports: []swarm.PortConfig{
|
Ports: []swarm.PortConfig{
|
||||||
{
|
{
|
||||||
PublishMode: "ingress",
|
PublishMode: "ingress",
|
||||||
PublishedPort: 80,
|
PublishedPort: 80,
|
||||||
TargetPort: 8080,
|
TargetPort: 8090,
|
||||||
Protocol: "tcp",
|
Protocol: "udp",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
ServiceStatus: &swarm.ServiceStatus{
|
||||||
|
RunningTasks: 2,
|
||||||
|
DesiredTasks: 4,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
{
|
||||||
info := map[string]ListInfo{
|
ID: "03_qux10",
|
||||||
"id_baz": {
|
Spec: swarm.ServiceSpec{
|
||||||
Mode: "global",
|
Annotations: swarm.Annotations{Name: "qux10"},
|
||||||
Replicas: "2/4",
|
Mode: swarm.ServiceMode{
|
||||||
|
Replicated: &swarm.ReplicatedService{},
|
||||||
|
},
|
||||||
|
TaskTemplate: swarm.TaskSpec{
|
||||||
|
Placement: &swarm.Placement{MaxReplicas: 1},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ServiceStatus: &swarm.ServiceStatus{
|
||||||
|
RunningTasks: 2,
|
||||||
|
DesiredTasks: 3,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"id_bar": {
|
{
|
||||||
Mode: "replicated",
|
ID: "04_qux2",
|
||||||
Replicas: "2/4",
|
Spec: swarm.ServiceSpec{
|
||||||
|
Annotations: swarm.Annotations{Name: "qux2"},
|
||||||
|
Mode: swarm.ServiceMode{
|
||||||
|
Replicated: &swarm.ReplicatedService{},
|
||||||
|
},
|
||||||
|
TaskTemplate: swarm.TaskSpec{
|
||||||
|
Placement: &swarm.Placement{MaxReplicas: 2},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ServiceStatus: &swarm.ServiceStatus{
|
||||||
|
RunningTasks: 3,
|
||||||
|
DesiredTasks: 3,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
out := bytes.NewBufferString("")
|
out := bytes.NewBufferString("")
|
||||||
testcase.context.Output = out
|
testcase.context.Output = out
|
||||||
err := ListFormatWrite(testcase.context, services, info)
|
err := ListFormatWrite(testcase.context, services)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
assert.Error(t, err, testcase.expected)
|
assert.Error(t, err, testcase.expected)
|
||||||
} else {
|
} else {
|
||||||
|
@ -137,9 +185,12 @@ bar
|
||||||
func TestServiceContextWriteJSON(t *testing.T) {
|
func TestServiceContextWriteJSON(t *testing.T) {
|
||||||
services := []swarm.Service{
|
services := []swarm.Service{
|
||||||
{
|
{
|
||||||
ID: "id_baz",
|
ID: "01_baz",
|
||||||
Spec: swarm.ServiceSpec{
|
Spec: swarm.ServiceSpec{
|
||||||
Annotations: swarm.Annotations{Name: "baz"},
|
Annotations: swarm.Annotations{Name: "baz"},
|
||||||
|
Mode: swarm.ServiceMode{
|
||||||
|
Global: &swarm.GlobalService{},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Endpoint: swarm.Endpoint{
|
Endpoint: swarm.Endpoint{
|
||||||
Ports: []swarm.PortConfig{
|
Ports: []swarm.PortConfig{
|
||||||
|
@ -151,11 +202,18 @@ func TestServiceContextWriteJSON(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
ServiceStatus: &swarm.ServiceStatus{
|
||||||
|
RunningTasks: 1,
|
||||||
|
DesiredTasks: 3,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "id_bar",
|
ID: "02_bar",
|
||||||
Spec: swarm.ServiceSpec{
|
Spec: swarm.ServiceSpec{
|
||||||
Annotations: swarm.Annotations{Name: "bar"},
|
Annotations: swarm.Annotations{Name: "bar"},
|
||||||
|
Mode: swarm.ServiceMode{
|
||||||
|
Replicated: &swarm.ReplicatedService{},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Endpoint: swarm.Endpoint{
|
Endpoint: swarm.Endpoint{
|
||||||
Ports: []swarm.PortConfig{
|
Ports: []swarm.PortConfig{
|
||||||
|
@ -167,25 +225,19 @@ func TestServiceContextWriteJSON(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
ServiceStatus: &swarm.ServiceStatus{
|
||||||
}
|
RunningTasks: 2,
|
||||||
info := map[string]ListInfo{
|
DesiredTasks: 4,
|
||||||
"id_baz": {
|
},
|
||||||
Mode: "global",
|
|
||||||
Replicas: "2/4",
|
|
||||||
},
|
|
||||||
"id_bar": {
|
|
||||||
Mode: "replicated",
|
|
||||||
Replicas: "2/4",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
expectedJSONs := []map[string]interface{}{
|
expectedJSONs := []map[string]interface{}{
|
||||||
{"ID": "id_baz", "Name": "baz", "Mode": "global", "Replicas": "2/4", "Image": "", "Ports": "*:80->8080/tcp"},
|
{"ID": "02_bar", "Name": "bar", "Mode": "replicated", "Replicas": "2/4", "Image": "", "Ports": "*:80->8080/tcp"},
|
||||||
{"ID": "id_bar", "Name": "bar", "Mode": "replicated", "Replicas": "2/4", "Image": "", "Ports": "*:80->8080/tcp"},
|
{"ID": "01_baz", "Name": "baz", "Mode": "global", "Replicas": "1/3", "Image": "", "Ports": "*:80->8080/tcp"},
|
||||||
}
|
}
|
||||||
|
|
||||||
out := bytes.NewBufferString("")
|
out := bytes.NewBufferString("")
|
||||||
err := ListFormatWrite(formatter.Context{Format: "{{json .}}", Output: out}, services, info)
|
err := ListFormatWrite(formatter.Context{Format: "{{json .}}", Output: out}, services)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -199,21 +251,35 @@ func TestServiceContextWriteJSON(t *testing.T) {
|
||||||
}
|
}
|
||||||
func TestServiceContextWriteJSONField(t *testing.T) {
|
func TestServiceContextWriteJSONField(t *testing.T) {
|
||||||
services := []swarm.Service{
|
services := []swarm.Service{
|
||||||
{ID: "id_baz", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "baz"}}},
|
{
|
||||||
{ID: "id_bar", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "bar"}}},
|
ID: "01_baz",
|
||||||
}
|
Spec: swarm.ServiceSpec{
|
||||||
info := map[string]ListInfo{
|
Annotations: swarm.Annotations{Name: "baz"},
|
||||||
"id_baz": {
|
Mode: swarm.ServiceMode{
|
||||||
Mode: "global",
|
Global: &swarm.GlobalService{},
|
||||||
Replicas: "2/4",
|
},
|
||||||
|
},
|
||||||
|
ServiceStatus: &swarm.ServiceStatus{
|
||||||
|
RunningTasks: 2,
|
||||||
|
DesiredTasks: 4,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"id_bar": {
|
{
|
||||||
Mode: "replicated",
|
ID: "24_bar",
|
||||||
Replicas: "2/4",
|
Spec: swarm.ServiceSpec{
|
||||||
|
Annotations: swarm.Annotations{Name: "bar"},
|
||||||
|
Mode: swarm.ServiceMode{
|
||||||
|
Replicated: &swarm.ReplicatedService{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ServiceStatus: &swarm.ServiceStatus{
|
||||||
|
RunningTasks: 2,
|
||||||
|
DesiredTasks: 4,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
out := bytes.NewBufferString("")
|
out := bytes.NewBufferString("")
|
||||||
err := ListFormatWrite(formatter.Context{Format: "{{json .Name}}", Output: out}, services, info)
|
err := ListFormatWrite(formatter.Context{Format: "{{json .Name}}", Output: out}, services)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,6 @@ package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"vbom.ml/util/sortorder"
|
|
||||||
|
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
|
@ -14,6 +10,7 @@ import (
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/docker/docker/api/types/swarm"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -44,43 +41,49 @@ func newListCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runList(dockerCli command.Cli, options listOptions) error {
|
func runList(dockerCli command.Cli, opts listOptions) error {
|
||||||
ctx := context.Background()
|
var (
|
||||||
client := dockerCli.Client()
|
apiClient = dockerCli.Client()
|
||||||
|
ctx = context.Background()
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
serviceFilters := options.filter.Value()
|
listOpts := types.ServiceListOptions{
|
||||||
services, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: serviceFilters})
|
Filters: opts.filter.Value(),
|
||||||
|
// When not running "quiet", also get service status (number of running
|
||||||
|
// and desired tasks). Note that this is only supported on API v1.41 and
|
||||||
|
// up; older API versions ignore this option, and we will have to collect
|
||||||
|
// the information manually below.
|
||||||
|
Status: !opts.quiet,
|
||||||
|
}
|
||||||
|
|
||||||
|
services, err := apiClient.ServiceList(ctx, listOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(services, func(i, j int) bool {
|
if listOpts.Status {
|
||||||
return sortorder.NaturalLess(services[i].Spec.Name, services[j].Spec.Name)
|
// Now that a request was made, we know what API version was used (either
|
||||||
})
|
// through configuration, or after client and daemon negotiated a version).
|
||||||
info := map[string]ListInfo{}
|
// If API version v1.41 or up was used; the daemon should already have done
|
||||||
if len(services) > 0 && !options.quiet {
|
// the legwork for us, and we don't have to calculate the number of desired
|
||||||
// only non-empty services and not quiet, should we call TaskList and NodeList api
|
// and running tasks. On older API versions, we need to do some extra requests
|
||||||
taskFilter := filters.NewArgs()
|
// to get that information.
|
||||||
for _, service := range services {
|
//
|
||||||
taskFilter.Add("service", service.ID)
|
// So theoretically, this step can be skipped based on API version, however,
|
||||||
}
|
// some of our unit tests don't set the API version, and there may be other
|
||||||
|
// situations where the client uses the "default" version. To account for
|
||||||
tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: taskFilter})
|
// these situations, we do a quick check for services that do not have
|
||||||
|
// a ServiceStatus set, and perform a lookup for those.
|
||||||
|
services, err = AppendServiceStatus(ctx, apiClient, services)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes, err := client.NodeList(ctx, types.NodeListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
info = GetServicesStatus(services, nodes, tasks)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
format := options.format
|
format := opts.format
|
||||||
if len(format) == 0 {
|
if len(format) == 0 {
|
||||||
if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !options.quiet {
|
if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !opts.quiet {
|
||||||
format = dockerCli.ConfigFile().ServicesFormat
|
format = dockerCli.ConfigFile().ServicesFormat
|
||||||
} else {
|
} else {
|
||||||
format = formatter.TableFormatKey
|
format = formatter.TableFormatKey
|
||||||
|
@ -89,54 +92,97 @@ func runList(dockerCli command.Cli, options listOptions) error {
|
||||||
|
|
||||||
servicesCtx := formatter.Context{
|
servicesCtx := formatter.Context{
|
||||||
Output: dockerCli.Out(),
|
Output: dockerCli.Out(),
|
||||||
Format: NewListFormat(format, options.quiet),
|
Format: NewListFormat(format, opts.quiet),
|
||||||
}
|
}
|
||||||
return ListFormatWrite(servicesCtx, services, info)
|
return ListFormatWrite(servicesCtx, services)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetServicesStatus returns a map of mode and replicas
|
// AppendServiceStatus propagates the ServiceStatus field for "services".
|
||||||
func GetServicesStatus(services []swarm.Service, nodes []swarm.Node, tasks []swarm.Task) map[string]ListInfo {
|
//
|
||||||
running := map[string]int{}
|
// If API version v1.41 or up is used, this information is already set by the
|
||||||
tasksNoShutdown := map[string]int{}
|
// daemon. On older API versions, we need to do some extra requests to get
|
||||||
|
// that information. Theoretically, this function can be skipped based on API
|
||||||
|
// version, however, some of our unit tests don't set the API version, and
|
||||||
|
// there may be other situations where the client uses the "default" version.
|
||||||
|
// To take these situations into account, we do a quick check for services
|
||||||
|
// that don't have ServiceStatus set, and perform a lookup for those.
|
||||||
|
// nolint: gocyclo
|
||||||
|
func AppendServiceStatus(ctx context.Context, c client.APIClient, services []swarm.Service) ([]swarm.Service, error) {
|
||||||
|
status := map[string]*swarm.ServiceStatus{}
|
||||||
|
taskFilter := filters.NewArgs()
|
||||||
|
for i, s := range services {
|
||||||
|
switch {
|
||||||
|
case s.ServiceStatus != nil:
|
||||||
|
// Server already returned service-status, so we don't
|
||||||
|
// have to look-up tasks for this service.
|
||||||
|
continue
|
||||||
|
case s.Spec.Mode.Replicated != nil:
|
||||||
|
// For replicated services, set the desired number of tasks;
|
||||||
|
// that way we can present this information in case we're unable
|
||||||
|
// to get a list of tasks from the server.
|
||||||
|
services[i].ServiceStatus = &swarm.ServiceStatus{DesiredTasks: *s.Spec.Mode.Replicated.Replicas}
|
||||||
|
status[s.ID] = &swarm.ServiceStatus{}
|
||||||
|
taskFilter.Add("service", s.ID)
|
||||||
|
case s.Spec.Mode.Global != nil:
|
||||||
|
// No such thing as number of desired tasks for global services
|
||||||
|
services[i].ServiceStatus = &swarm.ServiceStatus{}
|
||||||
|
status[s.ID] = &swarm.ServiceStatus{}
|
||||||
|
taskFilter.Add("service", s.ID)
|
||||||
|
default:
|
||||||
|
// Unknown task type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(status) == 0 {
|
||||||
|
// All services have their ServiceStatus set, so we're done
|
||||||
|
return services, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks, err := c.TaskList(ctx, types.TaskListOptions{Filters: taskFilter})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(tasks) == 0 {
|
||||||
|
return services, nil
|
||||||
|
}
|
||||||
|
activeNodes, err := getActiveNodes(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, task := range tasks {
|
||||||
|
if status[task.ServiceID] == nil {
|
||||||
|
// This should not happen in practice; either all services have
|
||||||
|
// a ServiceStatus set, or none of them.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// TODO: this should only be needed for "global" services. Replicated
|
||||||
|
// services have `Spec.Mode.Replicated.Replicas`, which should give this value.
|
||||||
|
if task.DesiredState != swarm.TaskStateShutdown {
|
||||||
|
status[task.ServiceID].DesiredTasks++
|
||||||
|
}
|
||||||
|
if _, nodeActive := activeNodes[task.NodeID]; nodeActive && task.Status.State == swarm.TaskStateRunning {
|
||||||
|
status[task.ServiceID].RunningTasks++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, service := range services {
|
||||||
|
if s := status[service.ID]; s != nil {
|
||||||
|
services[i].ServiceStatus = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return services, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getActiveNodes(ctx context.Context, c client.NodeAPIClient) (map[string]struct{}, error) {
|
||||||
|
nodes, err := c.NodeList(ctx, types.NodeListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
activeNodes := make(map[string]struct{})
|
activeNodes := make(map[string]struct{})
|
||||||
for _, n := range nodes {
|
for _, n := range nodes {
|
||||||
if n.Status.State != swarm.NodeStateDown {
|
if n.Status.State != swarm.NodeStateDown {
|
||||||
activeNodes[n.ID] = struct{}{}
|
activeNodes[n.ID] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return activeNodes, nil
|
||||||
for _, task := range tasks {
|
|
||||||
if task.DesiredState != swarm.TaskStateShutdown {
|
|
||||||
tasksNoShutdown[task.ServiceID]++
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, nodeActive := activeNodes[task.NodeID]; nodeActive && task.Status.State == swarm.TaskStateRunning {
|
|
||||||
running[task.ServiceID]++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
info := map[string]ListInfo{}
|
|
||||||
for _, service := range services {
|
|
||||||
info[service.ID] = ListInfo{}
|
|
||||||
if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil {
|
|
||||||
if service.Spec.TaskTemplate.Placement != nil && service.Spec.TaskTemplate.Placement.MaxReplicas > 0 {
|
|
||||||
info[service.ID] = ListInfo{
|
|
||||||
Mode: "replicated",
|
|
||||||
Replicas: fmt.Sprintf("%d/%d (max %d per node)", running[service.ID], *service.Spec.Mode.Replicated.Replicas, service.Spec.TaskTemplate.Placement.MaxReplicas),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
info[service.ID] = ListInfo{
|
|
||||||
Mode: "replicated",
|
|
||||||
Replicas: fmt.Sprintf("%d/%d", running[service.ID], *service.Spec.Mode.Replicated.Replicas),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if service.Spec.Mode.Global != nil {
|
|
||||||
info[service.ID] = ListInfo{
|
|
||||||
Mode: "global",
|
|
||||||
Replicas: fmt.Sprintf("%d/%d", running[service.ID], tasksNoShutdown[service.ID]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return info
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,19 @@ package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
|
// Import builders to get the builder function as package function
|
||||||
|
. "github.com/docker/cli/internal/test/builders"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/swarm"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/docker/api/types/versions"
|
||||||
"gotest.tools/assert"
|
"gotest.tools/assert"
|
||||||
|
is "gotest.tools/assert/cmp"
|
||||||
"gotest.tools/golden"
|
"gotest.tools/golden"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -22,7 +29,315 @@ func TestServiceListOrder(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
cmd := newListCommand(cli)
|
cmd := newListCommand(cli)
|
||||||
|
cmd.SetArgs([]string{})
|
||||||
cmd.Flags().Set("format", "{{.Name}}")
|
cmd.Flags().Set("format", "{{.Name}}")
|
||||||
assert.NilError(t, cmd.Execute())
|
assert.NilError(t, cmd.Execute())
|
||||||
golden.Assert(t, cli.OutBuffer().String(), "service-list-sort.golden")
|
golden.Assert(t, cli.OutBuffer().String(), "service-list-sort.golden")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestServiceListServiceStatus tests that the ServiceStatus struct is correctly
|
||||||
|
// propagated. For older API versions, the ServiceStatus is calculated locally,
|
||||||
|
// based on the tasks that are present in the swarm, and the nodes that they are
|
||||||
|
// running on.
|
||||||
|
// If the list command is ran with `--quiet` option, no attempt should be done to
|
||||||
|
// propagate the ServiceStatus struct if not present, and it should be set to an
|
||||||
|
// empty struct.
|
||||||
|
func TestServiceListServiceStatus(t *testing.T) {
|
||||||
|
type listResponse struct {
|
||||||
|
ID string
|
||||||
|
Replicas string
|
||||||
|
}
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
doc string
|
||||||
|
withQuiet bool
|
||||||
|
opts clusterOpts
|
||||||
|
cluster *cluster
|
||||||
|
expected []listResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []testCase{
|
||||||
|
{
|
||||||
|
// Getting no nodes, services or tasks back from the daemon should
|
||||||
|
// not cause any problems
|
||||||
|
doc: "empty cluster",
|
||||||
|
cluster: &cluster{}, // force an empty cluster
|
||||||
|
expected: []listResponse{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Services are running, but no active nodes were found. On API v1.40
|
||||||
|
// and below, this will cause looking up the "running" tasks to fail,
|
||||||
|
// as well as looking up "desired" tasks for global services.
|
||||||
|
doc: "API v1.40 no active nodes",
|
||||||
|
opts: clusterOpts{
|
||||||
|
apiVersion: "1.40",
|
||||||
|
activeNodes: 0,
|
||||||
|
runningTasks: 2,
|
||||||
|
desiredTasks: 4,
|
||||||
|
},
|
||||||
|
expected: []listResponse{
|
||||||
|
{ID: "replicated", Replicas: "0/4"},
|
||||||
|
{ID: "global", Replicas: "0/0"},
|
||||||
|
{ID: "none-id", Replicas: "0/0"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "API v1.40 3 active nodes, 1 task running",
|
||||||
|
opts: clusterOpts{
|
||||||
|
apiVersion: "1.40",
|
||||||
|
activeNodes: 3,
|
||||||
|
runningTasks: 1,
|
||||||
|
desiredTasks: 2,
|
||||||
|
},
|
||||||
|
expected: []listResponse{
|
||||||
|
{ID: "replicated", Replicas: "1/2"},
|
||||||
|
{ID: "global", Replicas: "1/3"},
|
||||||
|
{ID: "none-id", Replicas: "0/0"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "API v1.40 3 active nodes, all tasks running",
|
||||||
|
opts: clusterOpts{
|
||||||
|
apiVersion: "1.40",
|
||||||
|
activeNodes: 3,
|
||||||
|
runningTasks: 3,
|
||||||
|
desiredTasks: 3,
|
||||||
|
},
|
||||||
|
expected: []listResponse{
|
||||||
|
{ID: "replicated", Replicas: "3/3"},
|
||||||
|
{ID: "global", Replicas: "3/3"},
|
||||||
|
{ID: "none-id", Replicas: "0/0"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
// Services are running, but no active nodes were found. On API v1.41
|
||||||
|
// and up, the ServiceStatus is sent by the daemon, so this should not
|
||||||
|
// affect the results.
|
||||||
|
doc: "API v1.41 no active nodes",
|
||||||
|
opts: clusterOpts{
|
||||||
|
apiVersion: "1.41",
|
||||||
|
activeNodes: 0,
|
||||||
|
runningTasks: 2,
|
||||||
|
desiredTasks: 4,
|
||||||
|
},
|
||||||
|
expected: []listResponse{
|
||||||
|
{ID: "replicated", Replicas: "2/4"},
|
||||||
|
{ID: "global", Replicas: "0/0"},
|
||||||
|
{ID: "none-id", Replicas: "0/0"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "API v1.41 3 active nodes, 1 task running",
|
||||||
|
opts: clusterOpts{
|
||||||
|
apiVersion: "1.41",
|
||||||
|
activeNodes: 3,
|
||||||
|
runningTasks: 1,
|
||||||
|
desiredTasks: 2,
|
||||||
|
},
|
||||||
|
expected: []listResponse{
|
||||||
|
{ID: "replicated", Replicas: "1/2"},
|
||||||
|
{ID: "global", Replicas: "1/3"},
|
||||||
|
{ID: "none-id", Replicas: "0/0"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "API v1.41 3 active nodes, all tasks running",
|
||||||
|
opts: clusterOpts{
|
||||||
|
apiVersion: "1.41",
|
||||||
|
activeNodes: 3,
|
||||||
|
runningTasks: 3,
|
||||||
|
desiredTasks: 3,
|
||||||
|
},
|
||||||
|
expected: []listResponse{
|
||||||
|
{ID: "replicated", Replicas: "3/3"},
|
||||||
|
{ID: "global", Replicas: "3/3"},
|
||||||
|
{ID: "none-id", Replicas: "0/0"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
matrix := make([]testCase, 0)
|
||||||
|
for _, quiet := range []bool{false, true} {
|
||||||
|
for _, tc := range tests {
|
||||||
|
if quiet {
|
||||||
|
tc.withQuiet = quiet
|
||||||
|
tc.doc = tc.doc + " with quiet"
|
||||||
|
}
|
||||||
|
matrix = append(matrix, tc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range matrix {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.doc, func(t *testing.T) {
|
||||||
|
if tc.cluster == nil {
|
||||||
|
tc.cluster = generateCluster(t, tc.opts)
|
||||||
|
}
|
||||||
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
|
serviceListFunc: func(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) {
|
||||||
|
if !options.Status || versions.LessThan(tc.opts.apiVersion, "1.41") {
|
||||||
|
// Don't return "ServiceStatus" if not requested, or on older API versions
|
||||||
|
for i := range tc.cluster.services {
|
||||||
|
tc.cluster.services[i].ServiceStatus = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tc.cluster.services, nil
|
||||||
|
},
|
||||||
|
taskListFunc: func(context.Context, types.TaskListOptions) ([]swarm.Task, error) {
|
||||||
|
return tc.cluster.tasks, nil
|
||||||
|
},
|
||||||
|
nodeListFunc: func(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) {
|
||||||
|
return tc.cluster.nodes, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
cmd := newListCommand(cli)
|
||||||
|
cmd.SetArgs([]string{})
|
||||||
|
if tc.withQuiet {
|
||||||
|
cmd.SetArgs([]string{"--quiet"})
|
||||||
|
}
|
||||||
|
_ = cmd.Flags().Set("format", "{{ json .}}")
|
||||||
|
assert.NilError(t, cmd.Execute())
|
||||||
|
|
||||||
|
lines := strings.Split(strings.TrimSpace(cli.OutBuffer().String()), "\n")
|
||||||
|
jsonArr := fmt.Sprintf("[%s]", strings.Join(lines, ","))
|
||||||
|
results := make([]listResponse, 0)
|
||||||
|
assert.NilError(t, json.Unmarshal([]byte(jsonArr), &results))
|
||||||
|
|
||||||
|
if tc.withQuiet {
|
||||||
|
// With "quiet" enabled, ServiceStatus should not be propagated
|
||||||
|
for i := range tc.expected {
|
||||||
|
tc.expected[i].Replicas = "0/0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.Check(t, is.DeepEqual(tc.expected, results), "%+v", results)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type clusterOpts struct {
|
||||||
|
apiVersion string
|
||||||
|
activeNodes uint64
|
||||||
|
desiredTasks uint64
|
||||||
|
runningTasks uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type cluster struct {
|
||||||
|
services []swarm.Service
|
||||||
|
tasks []swarm.Task
|
||||||
|
nodes []swarm.Node
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateCluster(t *testing.T, opts clusterOpts) *cluster {
|
||||||
|
t.Helper()
|
||||||
|
c := cluster{
|
||||||
|
services: generateServices(t, opts),
|
||||||
|
nodes: generateNodes(t, opts.activeNodes),
|
||||||
|
}
|
||||||
|
c.tasks = generateTasks(t, c.services, c.nodes, opts)
|
||||||
|
return &c
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateServices(t *testing.T, opts clusterOpts) []swarm.Service {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// Can't have more global tasks than nodes
|
||||||
|
globalTasks := opts.runningTasks
|
||||||
|
if globalTasks > opts.activeNodes {
|
||||||
|
globalTasks = opts.activeNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
return []swarm.Service{
|
||||||
|
*Service(
|
||||||
|
ServiceID("replicated"),
|
||||||
|
ServiceName("01-replicated-service"),
|
||||||
|
ReplicatedService(opts.desiredTasks),
|
||||||
|
ServiceStatus(opts.desiredTasks, opts.runningTasks),
|
||||||
|
),
|
||||||
|
*Service(
|
||||||
|
ServiceID("global"),
|
||||||
|
ServiceName("02-global-service"),
|
||||||
|
GlobalService(),
|
||||||
|
ServiceStatus(opts.activeNodes, globalTasks),
|
||||||
|
),
|
||||||
|
*Service(
|
||||||
|
ServiceID("none-id"),
|
||||||
|
ServiceName("03-none-service"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateTasks(t *testing.T, services []swarm.Service, nodes []swarm.Node, opts clusterOpts) []swarm.Task {
|
||||||
|
t.Helper()
|
||||||
|
tasks := make([]swarm.Task, 0)
|
||||||
|
|
||||||
|
for _, s := range services {
|
||||||
|
if s.Spec.Mode.Replicated == nil && s.Spec.Mode.Global == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var runningTasks, failedTasks, desiredTasks uint64
|
||||||
|
|
||||||
|
// Set the number of desired tasks to generate, based on the service's mode
|
||||||
|
if s.Spec.Mode.Replicated != nil {
|
||||||
|
desiredTasks = *s.Spec.Mode.Replicated.Replicas
|
||||||
|
} else if s.Spec.Mode.Global != nil {
|
||||||
|
desiredTasks = opts.activeNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, n := range nodes {
|
||||||
|
if runningTasks < opts.runningTasks && n.Status.State != swarm.NodeStateDown {
|
||||||
|
tasks = append(tasks, swarm.Task{
|
||||||
|
NodeID: n.ID,
|
||||||
|
ServiceID: s.ID,
|
||||||
|
Status: swarm.TaskStatus{State: swarm.TaskStateRunning},
|
||||||
|
DesiredState: swarm.TaskStateRunning,
|
||||||
|
})
|
||||||
|
runningTasks++
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the number of "running" tasks is lower than the desired number
|
||||||
|
// of tasks of the service, fill in the remaining number of tasks
|
||||||
|
// with failed tasks. These tasks have a desired "running" state,
|
||||||
|
// and thus will be included when calculating the "desired" tasks
|
||||||
|
// for services.
|
||||||
|
if failedTasks < (desiredTasks - opts.runningTasks) {
|
||||||
|
tasks = append(tasks, swarm.Task{
|
||||||
|
NodeID: n.ID,
|
||||||
|
ServiceID: s.ID,
|
||||||
|
Status: swarm.TaskStatus{State: swarm.TaskStateFailed},
|
||||||
|
DesiredState: swarm.TaskStateRunning,
|
||||||
|
})
|
||||||
|
failedTasks++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also add tasks with DesiredState: Shutdown. These should not be
|
||||||
|
// counted as running or desired tasks.
|
||||||
|
tasks = append(tasks, swarm.Task{
|
||||||
|
NodeID: n.ID,
|
||||||
|
ServiceID: s.ID,
|
||||||
|
Status: swarm.TaskStatus{State: swarm.TaskStateShutdown},
|
||||||
|
DesiredState: swarm.TaskStateShutdown,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tasks
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateNodes generates a "nodes" endpoint API response with the requested
|
||||||
|
// number of "ready" nodes. In addition, a "down" node is generated.
|
||||||
|
func generateNodes(t *testing.T, activeNodes uint64) []swarm.Node {
|
||||||
|
t.Helper()
|
||||||
|
nodes := make([]swarm.Node, 0)
|
||||||
|
var i uint64
|
||||||
|
for i = 0; i < activeNodes; i++ {
|
||||||
|
nodes = append(nodes, swarm.Node{
|
||||||
|
ID: fmt.Sprintf("node-ready-%d", i),
|
||||||
|
Status: swarm.NodeStatus{State: swarm.NodeStateReady},
|
||||||
|
})
|
||||||
|
nodes = append(nodes, swarm.Node{
|
||||||
|
ID: fmt.Sprintf("node-down-%d", i),
|
||||||
|
Status: swarm.NodeStatus{State: swarm.NodeStateDown},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
|
|
@ -1,14 +1,28 @@
|
||||||
id: id_baz
|
id: 02_bar
|
||||||
name: baz
|
|
||||||
mode: global
|
|
||||||
replicas: 2/4
|
|
||||||
image:
|
|
||||||
ports: *:80->8080/tcp
|
|
||||||
|
|
||||||
id: id_bar
|
|
||||||
name: bar
|
name: bar
|
||||||
mode: replicated
|
mode: replicated
|
||||||
replicas: 2/4
|
replicas: 2/4
|
||||||
image:
|
image:
|
||||||
|
ports: *:80->8090/udp
|
||||||
|
|
||||||
|
id: 01_baz
|
||||||
|
name: baz
|
||||||
|
mode: global
|
||||||
|
replicas: 1/3
|
||||||
|
image:
|
||||||
ports: *:80->8080/tcp
|
ports: *:80->8080/tcp
|
||||||
|
|
||||||
|
id: 04_qux2
|
||||||
|
name: qux2
|
||||||
|
mode: replicated
|
||||||
|
replicas: 3/3 (max 2 per node)
|
||||||
|
image:
|
||||||
|
ports:
|
||||||
|
|
||||||
|
id: 03_qux10
|
||||||
|
name: qux10
|
||||||
|
mode: replicated
|
||||||
|
replicas: 2/3 (max 1 per node)
|
||||||
|
image:
|
||||||
|
ports:
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command/service"
|
|
||||||
"github.com/docker/compose-on-kubernetes/api/labels"
|
"github.com/docker/compose-on-kubernetes/api/labels"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/docker/docker/api/types/swarm"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
@ -154,35 +153,65 @@ const (
|
||||||
publishedOnRandomPortSuffix = "-random-ports"
|
publishedOnRandomPortSuffix = "-random-ports"
|
||||||
)
|
)
|
||||||
|
|
||||||
func convertToServices(replicas *appsv1beta2.ReplicaSetList, daemons *appsv1beta2.DaemonSetList, services *apiv1.ServiceList) ([]swarm.Service, map[string]service.ListInfo, error) {
|
func convertToServices(replicas *appsv1beta2.ReplicaSetList, daemons *appsv1beta2.DaemonSetList, services *apiv1.ServiceList) ([]swarm.Service, error) {
|
||||||
result := make([]swarm.Service, len(replicas.Items))
|
result := make([]swarm.Service, len(replicas.Items))
|
||||||
infos := make(map[string]service.ListInfo, len(replicas.Items)+len(daemons.Items))
|
|
||||||
for i, r := range replicas.Items {
|
for i, r := range replicas.Items {
|
||||||
s, err := convertToService(r.Labels[labels.ForServiceName], services, r.Spec.Template.Spec.Containers)
|
s, err := replicatedService(r, services)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
result[i] = *s
|
result[i] = *s
|
||||||
infos[s.ID] = service.ListInfo{
|
|
||||||
Mode: "replicated",
|
|
||||||
Replicas: fmt.Sprintf("%d/%d", r.Status.AvailableReplicas, r.Status.Replicas),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for _, d := range daemons.Items {
|
for _, d := range daemons.Items {
|
||||||
s, err := convertToService(d.Labels[labels.ForServiceName], services, d.Spec.Template.Spec.Containers)
|
s, err := globalService(d, services)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
result = append(result, *s)
|
result = append(result, *s)
|
||||||
infos[s.ID] = service.ListInfo{
|
|
||||||
Mode: "global",
|
|
||||||
Replicas: fmt.Sprintf("%d/%d", d.Status.NumberReady, d.Status.DesiredNumberScheduled),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
sort.Slice(result, func(i, j int) bool {
|
sort.Slice(result, func(i, j int) bool {
|
||||||
return result[i].ID < result[j].ID
|
return result[i].ID < result[j].ID
|
||||||
})
|
})
|
||||||
return result, infos, nil
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func uint64ptr(i int32) *uint64 {
|
||||||
|
var o uint64
|
||||||
|
if i > 0 {
|
||||||
|
o = uint64(i)
|
||||||
|
}
|
||||||
|
return &o
|
||||||
|
}
|
||||||
|
|
||||||
|
func replicatedService(r appsv1beta2.ReplicaSet, services *apiv1.ServiceList) (*swarm.Service, error) {
|
||||||
|
s, err := convertToService(r.Labels[labels.ForServiceName], services, r.Spec.Template.Spec.Containers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.Spec.Mode = swarm.ServiceMode{
|
||||||
|
Replicated: &swarm.ReplicatedService{Replicas: uint64ptr(r.Status.Replicas)},
|
||||||
|
}
|
||||||
|
s.ServiceStatus = &swarm.ServiceStatus{
|
||||||
|
RunningTasks: uint64(r.Status.AvailableReplicas),
|
||||||
|
DesiredTasks: uint64(r.Status.Replicas),
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func globalService(d appsv1beta2.DaemonSet, services *apiv1.ServiceList) (*swarm.Service, error) {
|
||||||
|
s, err := convertToService(d.Labels[labels.ForServiceName], services, d.Spec.Template.Spec.Containers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.Spec.Mode = swarm.ServiceMode{
|
||||||
|
Global: &swarm.GlobalService{},
|
||||||
|
}
|
||||||
|
s.ServiceStatus = &swarm.ServiceStatus{
|
||||||
|
RunningTasks: uint64(d.Status.NumberReady),
|
||||||
|
DesiredTasks: uint64(d.Status.DesiredNumberScheduled),
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertToService(serviceName string, services *apiv1.ServiceList, containers []apiv1.Container) (*swarm.Service, error) {
|
func convertToService(serviceName string, services *apiv1.ServiceList, containers []apiv1.Container) (*swarm.Service, error) {
|
||||||
|
|
|
@ -3,7 +3,6 @@ package kubernetes
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command/service"
|
|
||||||
"github.com/docker/compose-on-kubernetes/api/labels"
|
"github.com/docker/compose-on-kubernetes/api/labels"
|
||||||
"github.com/docker/docker/api/types/swarm"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
"gotest.tools/assert"
|
"gotest.tools/assert"
|
||||||
|
@ -19,49 +18,45 @@ func TestReplicasConversionNeedsAService(t *testing.T) {
|
||||||
Items: []appsv1beta2.ReplicaSet{makeReplicaSet("unknown", 0, 0)},
|
Items: []appsv1beta2.ReplicaSet{makeReplicaSet("unknown", 0, 0)},
|
||||||
}
|
}
|
||||||
services := apiv1.ServiceList{}
|
services := apiv1.ServiceList{}
|
||||||
_, _, err := convertToServices(&replicas, &appsv1beta2.DaemonSetList{}, &services)
|
_, err := convertToServices(&replicas, &appsv1beta2.DaemonSetList{}, &services)
|
||||||
assert.ErrorContains(t, err, "could not find service")
|
assert.ErrorContains(t, err, "could not find service")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestKubernetesServiceToSwarmServiceConversion(t *testing.T) {
|
func TestKubernetesServiceToSwarmServiceConversion(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
|
doc string
|
||||||
replicas *appsv1beta2.ReplicaSetList
|
replicas *appsv1beta2.ReplicaSetList
|
||||||
services *apiv1.ServiceList
|
services *apiv1.ServiceList
|
||||||
expectedServices []swarm.Service
|
expectedServices []swarm.Service
|
||||||
expectedListInfo map[string]service.ListInfo
|
|
||||||
}{
|
}{
|
||||||
// Match replicas with headless stack services
|
|
||||||
{
|
{
|
||||||
&appsv1beta2.ReplicaSetList{
|
doc: "Match replicas with headless stack services",
|
||||||
|
replicas: &appsv1beta2.ReplicaSetList{
|
||||||
Items: []appsv1beta2.ReplicaSet{
|
Items: []appsv1beta2.ReplicaSet{
|
||||||
makeReplicaSet("service1", 2, 5),
|
makeReplicaSet("service1", 2, 5),
|
||||||
makeReplicaSet("service2", 3, 3),
|
makeReplicaSet("service2", 3, 3),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&apiv1.ServiceList{
|
services: &apiv1.ServiceList{
|
||||||
Items: []apiv1.Service{
|
Items: []apiv1.Service{
|
||||||
makeKubeService("service1", "stack", "uid1", apiv1.ServiceTypeClusterIP, nil),
|
makeKubeService("service1", "stack", "uid1", apiv1.ServiceTypeClusterIP, nil),
|
||||||
makeKubeService("service2", "stack", "uid2", apiv1.ServiceTypeClusterIP, nil),
|
makeKubeService("service2", "stack", "uid2", apiv1.ServiceTypeClusterIP, nil),
|
||||||
makeKubeService("service3", "other-stack", "uid2", apiv1.ServiceTypeClusterIP, nil),
|
makeKubeService("service3", "other-stack", "uid2", apiv1.ServiceTypeClusterIP, nil),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[]swarm.Service{
|
expectedServices: []swarm.Service{
|
||||||
makeSwarmService("stack_service1", "uid1", nil),
|
makeSwarmService(t, "stack_service1", "uid1", withMode("replicated", 5), withStatus(2, 5)),
|
||||||
makeSwarmService("stack_service2", "uid2", nil),
|
makeSwarmService(t, "stack_service2", "uid2", withMode("replicated", 3), withStatus(3, 3)),
|
||||||
},
|
|
||||||
map[string]service.ListInfo{
|
|
||||||
"uid1": {Mode: "replicated", Replicas: "2/5"},
|
|
||||||
"uid2": {Mode: "replicated", Replicas: "3/3"},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Headless service and LoadBalancer Service are tied to the same Swarm service
|
|
||||||
{
|
{
|
||||||
&appsv1beta2.ReplicaSetList{
|
doc: "Headless service and LoadBalancer Service are tied to the same Swarm service",
|
||||||
|
replicas: &appsv1beta2.ReplicaSetList{
|
||||||
Items: []appsv1beta2.ReplicaSet{
|
Items: []appsv1beta2.ReplicaSet{
|
||||||
makeReplicaSet("service", 1, 1),
|
makeReplicaSet("service", 1, 1),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&apiv1.ServiceList{
|
services: &apiv1.ServiceList{
|
||||||
Items: []apiv1.Service{
|
Items: []apiv1.Service{
|
||||||
makeKubeService("service", "stack", "uid1", apiv1.ServiceTypeClusterIP, nil),
|
makeKubeService("service", "stack", "uid1", apiv1.ServiceTypeClusterIP, nil),
|
||||||
makeKubeService("service-published", "stack", "uid2", apiv1.ServiceTypeLoadBalancer, []apiv1.ServicePort{
|
makeKubeService("service-published", "stack", "uid2", apiv1.ServiceTypeLoadBalancer, []apiv1.ServicePort{
|
||||||
|
@ -73,29 +68,26 @@ func TestKubernetesServiceToSwarmServiceConversion(t *testing.T) {
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[]swarm.Service{
|
expectedServices: []swarm.Service{
|
||||||
makeSwarmService("stack_service", "uid1", []swarm.PortConfig{
|
makeSwarmService(t, "stack_service", "uid1",
|
||||||
{
|
withMode("replicated", 1),
|
||||||
|
withStatus(1, 1), withPort(swarm.PortConfig{
|
||||||
PublishMode: swarm.PortConfigPublishModeIngress,
|
PublishMode: swarm.PortConfigPublishModeIngress,
|
||||||
PublishedPort: 80,
|
PublishedPort: 80,
|
||||||
TargetPort: 80,
|
TargetPort: 80,
|
||||||
Protocol: swarm.PortConfigProtocolTCP,
|
Protocol: swarm.PortConfigProtocolTCP,
|
||||||
},
|
}),
|
||||||
}),
|
),
|
||||||
},
|
|
||||||
map[string]service.ListInfo{
|
|
||||||
"uid1": {Mode: "replicated", Replicas: "1/1"},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Headless service and NodePort Service are tied to the same Swarm service
|
|
||||||
|
|
||||||
{
|
{
|
||||||
&appsv1beta2.ReplicaSetList{
|
doc: "Headless service and NodePort Service are tied to the same Swarm service",
|
||||||
|
replicas: &appsv1beta2.ReplicaSetList{
|
||||||
Items: []appsv1beta2.ReplicaSet{
|
Items: []appsv1beta2.ReplicaSet{
|
||||||
makeReplicaSet("service", 1, 1),
|
makeReplicaSet("service", 1, 1),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&apiv1.ServiceList{
|
services: &apiv1.ServiceList{
|
||||||
Items: []apiv1.Service{
|
Items: []apiv1.Service{
|
||||||
makeKubeService("service", "stack", "uid1", apiv1.ServiceTypeClusterIP, nil),
|
makeKubeService("service", "stack", "uid1", apiv1.ServiceTypeClusterIP, nil),
|
||||||
makeKubeService("service-random-ports", "stack", "uid2", apiv1.ServiceTypeNodePort, []apiv1.ServicePort{
|
makeKubeService("service-random-ports", "stack", "uid2", apiv1.ServiceTypeNodePort, []apiv1.ServicePort{
|
||||||
|
@ -107,27 +99,28 @@ func TestKubernetesServiceToSwarmServiceConversion(t *testing.T) {
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[]swarm.Service{
|
expectedServices: []swarm.Service{
|
||||||
makeSwarmService("stack_service", "uid1", []swarm.PortConfig{
|
makeSwarmService(t, "stack_service", "uid1",
|
||||||
{
|
withMode("replicated", 1),
|
||||||
|
withStatus(1, 1),
|
||||||
|
withPort(swarm.PortConfig{
|
||||||
PublishMode: swarm.PortConfigPublishModeHost,
|
PublishMode: swarm.PortConfigPublishModeHost,
|
||||||
PublishedPort: 35666,
|
PublishedPort: 35666,
|
||||||
TargetPort: 80,
|
TargetPort: 80,
|
||||||
Protocol: swarm.PortConfigProtocolTCP,
|
Protocol: swarm.PortConfigProtocolTCP,
|
||||||
},
|
}),
|
||||||
}),
|
),
|
||||||
},
|
|
||||||
map[string]service.ListInfo{
|
|
||||||
"uid1": {Mode: "replicated", Replicas: "1/1"},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
swarmServices, listInfo, err := convertToServices(tc.replicas, &appsv1beta2.DaemonSetList{}, tc.services)
|
tc := tc
|
||||||
assert.NilError(t, err)
|
t.Run(tc.doc, func(t *testing.T) {
|
||||||
assert.DeepEqual(t, tc.expectedServices, swarmServices)
|
swarmServices, err := convertToServices(tc.replicas, &appsv1beta2.DaemonSetList{}, tc.services)
|
||||||
assert.DeepEqual(t, tc.expectedListInfo, listInfo)
|
assert.NilError(t, err)
|
||||||
|
assert.DeepEqual(t, tc.expectedServices, swarmServices)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,8 +165,46 @@ func makeKubeService(service, stack, uid string, serviceType apiv1.ServiceType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeSwarmService(service, id string, ports []swarm.PortConfig) swarm.Service {
|
func withMode(mode string, replicas uint64) func(*swarm.Service) {
|
||||||
return swarm.Service{
|
return func(service *swarm.Service) {
|
||||||
|
switch mode {
|
||||||
|
case "global":
|
||||||
|
service.Spec.Mode = swarm.ServiceMode{
|
||||||
|
Global: &swarm.GlobalService{},
|
||||||
|
}
|
||||||
|
case "replicated":
|
||||||
|
service.Spec.Mode = swarm.ServiceMode{
|
||||||
|
Replicated: &swarm.ReplicatedService{Replicas: &replicas},
|
||||||
|
}
|
||||||
|
withStatus(0, replicas)
|
||||||
|
default:
|
||||||
|
service.Spec.Mode = swarm.ServiceMode{}
|
||||||
|
withStatus(0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withPort(port swarm.PortConfig) func(*swarm.Service) {
|
||||||
|
return func(service *swarm.Service) {
|
||||||
|
if service.Endpoint.Ports == nil {
|
||||||
|
service.Endpoint.Ports = make([]swarm.PortConfig, 0)
|
||||||
|
}
|
||||||
|
service.Endpoint.Ports = append(service.Endpoint.Ports, port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withStatus(running, desired uint64) func(*swarm.Service) {
|
||||||
|
return func(service *swarm.Service) {
|
||||||
|
service.ServiceStatus = &swarm.ServiceStatus{
|
||||||
|
RunningTasks: running,
|
||||||
|
DesiredTasks: desired,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeSwarmService(t *testing.T, service, id string, opts ...func(*swarm.Service)) swarm.Service {
|
||||||
|
t.Helper()
|
||||||
|
s := swarm.Service{
|
||||||
ID: id,
|
ID: id,
|
||||||
Spec: swarm.ServiceSpec{
|
Spec: swarm.ServiceSpec{
|
||||||
Annotations: swarm.Annotations{
|
Annotations: swarm.Annotations{
|
||||||
|
@ -185,8 +216,9 @@ func makeSwarmService(service, id string, ports []swarm.PortConfig) swarm.Servic
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Endpoint: swarm.Endpoint{
|
|
||||||
Ports: ports,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&s)
|
||||||
|
}
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,16 +109,12 @@ func RunServices(dockerCli *KubeCli, opts options.Services) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert Replicas sets and kubernetes services to swarm services and formatter information
|
// Convert Replicas sets and kubernetes services to swarm services and formatter information
|
||||||
services, info, err := convertToServices(replicasList, daemonsList, servicesList)
|
services, err := convertToServices(replicasList, daemonsList, servicesList)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
services = filterServicesByName(services, filters.Get("name"), stackName)
|
services = filterServicesByName(services, filters.Get("name"), stackName)
|
||||||
|
|
||||||
if opts.Quiet {
|
|
||||||
info = map[string]service.ListInfo{}
|
|
||||||
}
|
|
||||||
|
|
||||||
format := opts.Format
|
format := opts.Format
|
||||||
if len(format) == 0 {
|
if len(format) == 0 {
|
||||||
if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !opts.Quiet {
|
if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !opts.Quiet {
|
||||||
|
@ -132,7 +128,7 @@ func RunServices(dockerCli *KubeCli, opts options.Services) error {
|
||||||
Output: dockerCli.Out(),
|
Output: dockerCli.Out(),
|
||||||
Format: service.NewListFormat(format, opts.Quiet),
|
Format: service.NewListFormat(format, opts.Quiet),
|
||||||
}
|
}
|
||||||
return service.ListFormatWrite(servicesCtx, services, info)
|
return service.ListFormatWrite(servicesCtx, services)
|
||||||
}
|
}
|
||||||
|
|
||||||
func filterServicesByName(services []swarm.Service, names []string, stackName string) []swarm.Service {
|
func filterServicesByName(services []swarm.Service, names []string, stackName string) []swarm.Service {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
"github.com/docker/cli/cli/config/configfile"
|
"github.com/docker/cli/cli/config/configfile"
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
|
|
||||||
// Import builders to get the builder function as package function
|
// Import builders to get the builder function as package function
|
||||||
. "github.com/docker/cli/internal/test/builders"
|
. "github.com/docker/cli/internal/test/builders"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
|
@ -35,17 +36,20 @@ func TestStackServicesErrors(t *testing.T) {
|
||||||
{
|
{
|
||||||
args: []string{"foo"},
|
args: []string{"foo"},
|
||||||
serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) {
|
serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) {
|
||||||
return []swarm.Service{*Service()}, nil
|
return []swarm.Service{*Service(GlobalService())}, nil
|
||||||
},
|
},
|
||||||
nodeListFunc: func(options types.NodeListOptions) ([]swarm.Node, error) {
|
nodeListFunc: func(options types.NodeListOptions) ([]swarm.Node, error) {
|
||||||
return nil, errors.Errorf("error getting nodes")
|
return nil, errors.Errorf("error getting nodes")
|
||||||
},
|
},
|
||||||
|
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
|
||||||
|
return []swarm.Task{*Task()}, nil
|
||||||
|
},
|
||||||
expectedError: "error getting nodes",
|
expectedError: "error getting nodes",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
args: []string{"foo"},
|
args: []string{"foo"},
|
||||||
serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) {
|
serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) {
|
||||||
return []swarm.Service{*Service()}, nil
|
return []swarm.Service{*Service(GlobalService())}, nil
|
||||||
},
|
},
|
||||||
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
|
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
|
||||||
return nil, errors.Errorf("error getting tasks")
|
return nil, errors.Errorf("error getting tasks")
|
||||||
|
@ -65,18 +69,21 @@ func TestStackServicesErrors(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
tc := tc
|
||||||
serviceListFunc: tc.serviceListFunc,
|
t.Run(tc.expectedError, func(t *testing.T) {
|
||||||
nodeListFunc: tc.nodeListFunc,
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
taskListFunc: tc.taskListFunc,
|
serviceListFunc: tc.serviceListFunc,
|
||||||
|
nodeListFunc: tc.nodeListFunc,
|
||||||
|
taskListFunc: tc.taskListFunc,
|
||||||
|
})
|
||||||
|
cmd := newServicesCommand(cli, &orchestrator)
|
||||||
|
cmd.SetArgs(tc.args)
|
||||||
|
for key, value := range tc.flags {
|
||||||
|
cmd.Flags().Set(key, value)
|
||||||
|
}
|
||||||
|
cmd.SetOutput(ioutil.Discard)
|
||||||
|
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||||
})
|
})
|
||||||
cmd := newServicesCommand(cli, &orchestrator)
|
|
||||||
cmd.SetArgs(tc.args)
|
|
||||||
for key, value := range tc.flags {
|
|
||||||
cmd.Flags().Set(key, value)
|
|
||||||
}
|
|
||||||
cmd.SetOutput(ioutil.Discard)
|
|
||||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,55 +3,59 @@ package swarm
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/service"
|
"github.com/docker/cli/cli/command/service"
|
||||||
"github.com/docker/cli/cli/command/stack/formatter"
|
"github.com/docker/cli/cli/command/stack/formatter"
|
||||||
"github.com/docker/cli/cli/command/stack/options"
|
"github.com/docker/cli/cli/command/stack/options"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/filters"
|
|
||||||
"vbom.ml/util/sortorder"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// RunServices is the swarm implementation of docker stack services
|
// RunServices is the swarm implementation of docker stack services
|
||||||
func RunServices(dockerCli command.Cli, opts options.Services) error {
|
func RunServices(dockerCli command.Cli, opts options.Services) error {
|
||||||
ctx := context.Background()
|
var (
|
||||||
client := dockerCli.Client()
|
err error
|
||||||
|
ctx = context.Background()
|
||||||
|
client = dockerCli.Client()
|
||||||
|
)
|
||||||
|
|
||||||
filter := getStackFilterFromOpt(opts.Namespace, opts.Filter)
|
listOpts := types.ServiceListOptions{
|
||||||
services, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: filter})
|
Filters: getStackFilterFromOpt(opts.Namespace, opts.Filter),
|
||||||
|
// When not running "quiet", also get service status (number of running
|
||||||
|
// and desired tasks). Note that this is only supported on API v1.41 and
|
||||||
|
// up; older API versions ignore this option, and we will have to collect
|
||||||
|
// the information manually below.
|
||||||
|
Status: !opts.Quiet,
|
||||||
|
}
|
||||||
|
|
||||||
|
services, err := client.ServiceList(ctx, listOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// if no services in this stack, print message and exit 0
|
// if no services in this stack, print message and exit 0
|
||||||
if len(services) == 0 {
|
if len(services) == 0 {
|
||||||
fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", opts.Namespace)
|
_, _ = fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", opts.Namespace)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(services, func(i, j int) bool {
|
if listOpts.Status {
|
||||||
return sortorder.NaturalLess(services[i].Spec.Name, services[j].Spec.Name)
|
// Now that a request was made, we know what API version was used (either
|
||||||
})
|
// through configuration, or after client and daemon negotiated a version).
|
||||||
info := map[string]service.ListInfo{}
|
// If API version v1.41 or up was used; the daemon should already have done
|
||||||
if !opts.Quiet {
|
// the legwork for us, and we don't have to calculate the number of desired
|
||||||
taskFilter := filters.NewArgs()
|
// and running tasks. On older API versions, we need to do some extra requests
|
||||||
for _, service := range services {
|
// to get that information.
|
||||||
taskFilter.Add("service", service.ID)
|
//
|
||||||
}
|
// So theoretically, this step can be skipped based on API version, however,
|
||||||
|
// some of our unit tests don't set the API version, and there may be other
|
||||||
tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: taskFilter})
|
// situations where the client uses the "default" version. To account for
|
||||||
|
// these situations, we do a quick check for services that do not have
|
||||||
|
// a ServiceStatus set, and perform a lookup for those.
|
||||||
|
services, err = service.AppendServiceStatus(ctx, client, services)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes, err := client.NodeList(ctx, types.NodeListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
info = service.GetServicesStatus(services, nodes, tasks)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
format := opts.Format
|
format := opts.Format
|
||||||
|
@ -67,5 +71,5 @@ func RunServices(dockerCli command.Cli, opts options.Services) error {
|
||||||
Output: dockerCli.Out(),
|
Output: dockerCli.Out(),
|
||||||
Format: service.NewListFormat(format, opts.Quiet),
|
Format: service.NewListFormat(format, opts.Quiet),
|
||||||
}
|
}
|
||||||
return service.ListFormatWrite(servicesCtx, services, info)
|
return service.ListFormatWrite(servicesCtx, services)
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,10 +46,31 @@ func ServiceLabels(labels map[string]string) func(*swarm.Service) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReplicatedService sets the number of replicas for the service
|
// GlobalService sets the service to use "global" mode
|
||||||
|
func GlobalService() func(*swarm.Service) {
|
||||||
|
return func(service *swarm.Service) {
|
||||||
|
service.Spec.Mode = swarm.ServiceMode{Global: &swarm.GlobalService{}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplicatedService sets the service to use "replicated" mode with the specified number of replicas
|
||||||
func ReplicatedService(replicas uint64) func(*swarm.Service) {
|
func ReplicatedService(replicas uint64) func(*swarm.Service) {
|
||||||
return func(service *swarm.Service) {
|
return func(service *swarm.Service) {
|
||||||
service.Spec.Mode = swarm.ServiceMode{Replicated: &swarm.ReplicatedService{Replicas: &replicas}}
|
service.Spec.Mode = swarm.ServiceMode{Replicated: &swarm.ReplicatedService{Replicas: &replicas}}
|
||||||
|
if service.ServiceStatus == nil {
|
||||||
|
service.ServiceStatus = &swarm.ServiceStatus{}
|
||||||
|
}
|
||||||
|
service.ServiceStatus.DesiredTasks = replicas
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceStatus sets the services' ServiceStatus (API v1.41 and above)
|
||||||
|
func ServiceStatus(desired, running uint64) func(*swarm.Service) {
|
||||||
|
return func(service *swarm.Service) {
|
||||||
|
service.ServiceStatus = &swarm.ServiceStatus{
|
||||||
|
RunningTasks: running,
|
||||||
|
DesiredTasks: desired,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue