Import docker/docker/cli
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
This commit is contained in:
30
cli/command/service/cmd.go
Normal file
30
cli/command/service/cmd.go
Normal file
@ -0,0 +1,30 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
)
|
||||
|
||||
// NewServiceCommand returns a cobra command for `service` subcommands
|
||||
func NewServiceCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "service",
|
||||
Short: "Manage services",
|
||||
Args: cli.NoArgs,
|
||||
RunE: dockerCli.ShowHelp,
|
||||
Tags: map[string]string{"version": "1.24"},
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newCreateCommand(dockerCli),
|
||||
newInspectCommand(dockerCli),
|
||||
newPsCommand(dockerCli),
|
||||
newListCommand(dockerCli),
|
||||
newRemoveCommand(dockerCli),
|
||||
newScaleCommand(dockerCli),
|
||||
newUpdateCommand(dockerCli),
|
||||
newLogsCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
118
cli/command/service/create.go
Normal file
118
cli/command/service/create.go
Normal file
@ -0,0 +1,118 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
opts := newServiceOptions()
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "create [OPTIONS] IMAGE [COMMAND] [ARG...]",
|
||||
Short: "Create a new service",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.image = args[0]
|
||||
if len(args) > 1 {
|
||||
opts.args = args[1:]
|
||||
}
|
||||
return runCreate(dockerCli, cmd.Flags(), opts)
|
||||
},
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.StringVar(&opts.mode, flagMode, "replicated", "Service mode (replicated or global)")
|
||||
flags.StringVar(&opts.name, flagName, "", "Service name")
|
||||
|
||||
addServiceFlags(flags, opts, buildServiceDefaultFlagMapping())
|
||||
|
||||
flags.VarP(&opts.labels, flagLabel, "l", "Service labels")
|
||||
flags.Var(&opts.containerLabels, flagContainerLabel, "Container labels")
|
||||
flags.VarP(&opts.env, flagEnv, "e", "Set environment variables")
|
||||
flags.Var(&opts.envFile, flagEnvFile, "Read in a file of environment variables")
|
||||
flags.Var(&opts.mounts, flagMount, "Attach a filesystem mount to the service")
|
||||
flags.Var(&opts.constraints, flagConstraint, "Placement constraints")
|
||||
flags.Var(&opts.placementPrefs, flagPlacementPref, "Add a placement preference")
|
||||
flags.SetAnnotation(flagPlacementPref, "version", []string{"1.28"})
|
||||
flags.Var(&opts.networks, flagNetwork, "Network attachments")
|
||||
flags.Var(&opts.secrets, flagSecret, "Specify secrets to expose to the service")
|
||||
flags.SetAnnotation(flagSecret, "version", []string{"1.25"})
|
||||
flags.VarP(&opts.endpoint.publishPorts, flagPublish, "p", "Publish a port as a node port")
|
||||
flags.Var(&opts.groups, flagGroup, "Set one or more supplementary user groups for the container")
|
||||
flags.SetAnnotation(flagGroup, "version", []string{"1.25"})
|
||||
flags.Var(&opts.dns, flagDNS, "Set custom DNS servers")
|
||||
flags.SetAnnotation(flagDNS, "version", []string{"1.25"})
|
||||
flags.Var(&opts.dnsOption, flagDNSOption, "Set DNS options")
|
||||
flags.SetAnnotation(flagDNSOption, "version", []string{"1.25"})
|
||||
flags.Var(&opts.dnsSearch, flagDNSSearch, "Set custom DNS search domains")
|
||||
flags.SetAnnotation(flagDNSSearch, "version", []string{"1.25"})
|
||||
flags.Var(&opts.hosts, flagHost, "Set one or more custom host-to-IP mappings (host:ip)")
|
||||
flags.SetAnnotation(flagHost, "version", []string{"1.25"})
|
||||
|
||||
flags.SetInterspersed(false)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runCreate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *serviceOptions) error {
|
||||
apiClient := dockerCli.Client()
|
||||
createOpts := types.ServiceCreateOptions{}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
service, err := opts.ToService(ctx, apiClient, flags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
specifiedSecrets := opts.secrets.Value()
|
||||
if len(specifiedSecrets) > 0 {
|
||||
// parse and validate secrets
|
||||
secrets, err := ParseSecrets(apiClient, specifiedSecrets)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
service.TaskTemplate.ContainerSpec.Secrets = secrets
|
||||
|
||||
}
|
||||
|
||||
if err := resolveServiceImageDigest(dockerCli, &service); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// only send auth if flag was set
|
||||
if opts.registryAuth {
|
||||
// Retrieve encoded auth token from the image reference
|
||||
encodedAuth, err := command.RetrieveAuthTokenFromImage(ctx, dockerCli, opts.image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
createOpts.EncodedRegistryAuth = encodedAuth
|
||||
}
|
||||
|
||||
response, err := apiClient.ServiceCreate(ctx, service, createOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, warning := range response.Warnings {
|
||||
fmt.Fprintln(dockerCli.Err(), warning)
|
||||
}
|
||||
|
||||
fmt.Fprintf(dockerCli.Out(), "%s\n", response.ID)
|
||||
|
||||
if opts.detach {
|
||||
if !flags.Changed("detach") {
|
||||
fmt.Fprintln(dockerCli.Err(), "Since --detach=false was not specified, tasks will be created in the background.\n"+
|
||||
"In a future release, --detach=false will become the default.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return waitOnService(ctx, dockerCli, response.ID, opts)
|
||||
}
|
||||
39
cli/command/service/helpers.go
Normal file
39
cli/command/service/helpers.go
Normal file
@ -0,0 +1,39 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/docker/cli/command/service/progress"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// waitOnService waits for the service to converge. It outputs a progress bar,
|
||||
// if appopriate based on the CLI flags.
|
||||
func waitOnService(ctx context.Context, dockerCli *command.DockerCli, serviceID string, opts *serviceOptions) error {
|
||||
errChan := make(chan error, 1)
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
|
||||
go func() {
|
||||
errChan <- progress.ServiceProgress(ctx, dockerCli.Client(), serviceID, pipeWriter)
|
||||
}()
|
||||
|
||||
if opts.quiet {
|
||||
go func() {
|
||||
for {
|
||||
var buf [1024]byte
|
||||
if _, err := pipeReader.Read(buf[:]); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return <-errChan
|
||||
}
|
||||
|
||||
err := jsonmessage.DisplayJSONMessagesToStream(pipeReader, dockerCli.Out(), nil)
|
||||
if err == nil {
|
||||
err = <-errChan
|
||||
}
|
||||
return err
|
||||
}
|
||||
94
cli/command/service/inspect.go
Normal file
94
cli/command/service/inspect.go
Normal file
@ -0,0 +1,94 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/docker/cli/command/formatter"
|
||||
apiclient "github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type inspectOptions struct {
|
||||
refs []string
|
||||
format string
|
||||
pretty bool
|
||||
}
|
||||
|
||||
func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts inspectOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "inspect [OPTIONS] SERVICE [SERVICE...]",
|
||||
Short: "Display detailed information on one or more services",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.refs = args
|
||||
|
||||
if opts.pretty && len(opts.format) > 0 {
|
||||
return errors.Errorf("--format is incompatible with human friendly format")
|
||||
}
|
||||
return runInspect(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template")
|
||||
flags.BoolVar(&opts.pretty, "pretty", false, "Print the information in a human friendly format")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
if opts.pretty {
|
||||
opts.format = "pretty"
|
||||
}
|
||||
|
||||
getRef := func(ref string) (interface{}, []byte, error) {
|
||||
// Service inspect shows defaults values in empty fields.
|
||||
service, _, err := client.ServiceInspectWithRaw(ctx, ref, types.ServiceInspectOptions{InsertDefaults: true})
|
||||
if err == nil || !apiclient.IsErrServiceNotFound(err) {
|
||||
return service, nil, err
|
||||
}
|
||||
return nil, nil, errors.Errorf("Error: no such service: %s", ref)
|
||||
}
|
||||
|
||||
getNetwork := func(ref string) (interface{}, []byte, error) {
|
||||
network, _, err := client.NetworkInspectWithRaw(ctx, ref, false)
|
||||
if err == nil || !apiclient.IsErrNetworkNotFound(err) {
|
||||
return network, nil, err
|
||||
}
|
||||
return nil, nil, errors.Errorf("Error: no such network: %s", ref)
|
||||
}
|
||||
|
||||
f := opts.format
|
||||
if len(f) == 0 {
|
||||
f = "raw"
|
||||
if len(dockerCli.ConfigFile().ServiceInspectFormat) > 0 {
|
||||
f = dockerCli.ConfigFile().ServiceInspectFormat
|
||||
}
|
||||
}
|
||||
|
||||
// check if the user is trying to apply a template to the pretty format, which
|
||||
// is not supported
|
||||
if strings.HasPrefix(f, "pretty") && f != "pretty" {
|
||||
return errors.Errorf("Cannot supply extra formatting options to the pretty template")
|
||||
}
|
||||
|
||||
serviceCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.NewServiceFormat(f),
|
||||
}
|
||||
|
||||
if err := formatter.ServiceInspectWrite(serviceCtx, opts.refs, getRef, getNetwork); err != nil {
|
||||
return cli.StatusError{StatusCode: 1, Status: err.Error()}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
140
cli/command/service/inspect_test.go
Normal file
140
cli/command/service/inspect_test.go
Normal file
@ -0,0 +1,140 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/cli/command/formatter"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func formatServiceInspect(t *testing.T, format formatter.Format, now time.Time) string {
|
||||
b := new(bytes.Buffer)
|
||||
|
||||
endpointSpec := &swarm.EndpointSpec{
|
||||
Mode: "vip",
|
||||
Ports: []swarm.PortConfig{
|
||||
{
|
||||
Protocol: swarm.PortConfigProtocolTCP,
|
||||
TargetPort: 5000,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
two := uint64(2)
|
||||
|
||||
s := swarm.Service{
|
||||
ID: "de179gar9d0o7ltdybungplod",
|
||||
Meta: swarm.Meta{
|
||||
Version: swarm.Version{Index: 315},
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
Spec: swarm.ServiceSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: "my_service",
|
||||
Labels: map[string]string{"com.label": "foo"},
|
||||
},
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
ContainerSpec: swarm.ContainerSpec{
|
||||
Image: "foo/bar@sha256:this_is_a_test",
|
||||
},
|
||||
Networks: []swarm.NetworkAttachmentConfig{
|
||||
{
|
||||
Target: "5vpyomhb6ievnk0i0o60gcnei",
|
||||
Aliases: []string{"web"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Mode: swarm.ServiceMode{
|
||||
Replicated: &swarm.ReplicatedService{
|
||||
Replicas: &two,
|
||||
},
|
||||
},
|
||||
EndpointSpec: endpointSpec,
|
||||
},
|
||||
Endpoint: swarm.Endpoint{
|
||||
Spec: *endpointSpec,
|
||||
Ports: []swarm.PortConfig{
|
||||
{
|
||||
Protocol: swarm.PortConfigProtocolTCP,
|
||||
TargetPort: 5000,
|
||||
PublishedPort: 30000,
|
||||
},
|
||||
},
|
||||
VirtualIPs: []swarm.EndpointVirtualIP{
|
||||
{
|
||||
NetworkID: "6o4107cj2jx9tihgb0jyts6pj",
|
||||
Addr: "10.255.0.4/16",
|
||||
},
|
||||
},
|
||||
},
|
||||
UpdateStatus: &swarm.UpdateStatus{
|
||||
StartedAt: &now,
|
||||
CompletedAt: &now,
|
||||
},
|
||||
}
|
||||
|
||||
ctx := formatter.Context{
|
||||
Output: b,
|
||||
Format: format,
|
||||
}
|
||||
|
||||
err := formatter.ServiceInspectWrite(ctx, []string{"de179gar9d0o7ltdybungplod"},
|
||||
func(ref string) (interface{}, []byte, error) {
|
||||
return s, nil, nil
|
||||
},
|
||||
func(ref string) (interface{}, []byte, error) {
|
||||
return types.NetworkResource{
|
||||
ID: "5vpyomhb6ievnk0i0o60gcnei",
|
||||
Name: "mynetwork",
|
||||
}, nil, nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func TestPrettyPrintWithNoUpdateConfig(t *testing.T) {
|
||||
s := formatServiceInspect(t, formatter.NewServiceFormat("pretty"), time.Now())
|
||||
if strings.Contains(s, "UpdateStatus") {
|
||||
t.Fatal("Pretty print failed before parsing UpdateStatus")
|
||||
}
|
||||
if !strings.Contains(s, "mynetwork") {
|
||||
t.Fatal("network name not found in inspect output")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONFormatWithNoUpdateConfig(t *testing.T) {
|
||||
now := time.Now()
|
||||
// s1: [{"ID":..}]
|
||||
// s2: {"ID":..}
|
||||
s1 := formatServiceInspect(t, formatter.NewServiceFormat(""), now)
|
||||
t.Log("// s1")
|
||||
t.Logf("%s", s1)
|
||||
s2 := formatServiceInspect(t, formatter.NewServiceFormat("{{json .}}"), now)
|
||||
t.Log("// s2")
|
||||
t.Logf("%s", s2)
|
||||
var m1Wrap []map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(s1), &m1Wrap); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(m1Wrap) != 1 {
|
||||
t.Fatalf("strange s1=%s", s1)
|
||||
}
|
||||
m1 := m1Wrap[0]
|
||||
t.Logf("m1=%+v", m1)
|
||||
var m2 map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(s2), &m2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("m2=%+v", m2)
|
||||
assert.Equal(t, m1, m2)
|
||||
}
|
||||
130
cli/command/service/list.go
Normal file
130
cli/command/service/list.go
Normal file
@ -0,0 +1,130 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/docker/cli/command/formatter"
|
||||
"github.com/docker/docker/opts"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type listOptions struct {
|
||||
quiet bool
|
||||
format string
|
||||
filter opts.FilterOpt
|
||||
}
|
||||
|
||||
func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
opts := listOptions{filter: opts.NewFilterOpt()}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "ls [OPTIONS]",
|
||||
Aliases: []string{"list"},
|
||||
Short: "List services",
|
||||
Args: cli.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runList(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs")
|
||||
flags.StringVar(&opts.format, "format", "", "Pretty-print services using a Go template")
|
||||
flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runList(dockerCli *command.DockerCli, opts listOptions) error {
|
||||
ctx := context.Background()
|
||||
client := dockerCli.Client()
|
||||
|
||||
serviceFilters := opts.filter.Value()
|
||||
serviceFilters.Add("runtime", string(swarm.RuntimeContainer))
|
||||
services, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: serviceFilters})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
info := map[string]formatter.ServiceListInfo{}
|
||||
if len(services) > 0 && !opts.quiet {
|
||||
// only non-empty services and not quiet, should we call TaskList and NodeList api
|
||||
taskFilter := filters.NewArgs()
|
||||
for _, service := range services {
|
||||
taskFilter.Add("service", service.ID)
|
||||
}
|
||||
|
||||
tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: taskFilter})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nodes, err := client.NodeList(ctx, types.NodeListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
info = GetServicesStatus(services, nodes, tasks)
|
||||
}
|
||||
|
||||
format := opts.format
|
||||
if len(format) == 0 {
|
||||
if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !opts.quiet {
|
||||
format = dockerCli.ConfigFile().ServicesFormat
|
||||
} else {
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
}
|
||||
|
||||
servicesCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.NewServiceListFormat(format, opts.quiet),
|
||||
}
|
||||
return formatter.ServiceListWrite(servicesCtx, services, info)
|
||||
}
|
||||
|
||||
// GetServicesStatus returns a map of mode and replicas
|
||||
func GetServicesStatus(services []swarm.Service, nodes []swarm.Node, tasks []swarm.Task) map[string]formatter.ServiceListInfo {
|
||||
running := map[string]int{}
|
||||
tasksNoShutdown := map[string]int{}
|
||||
|
||||
activeNodes := make(map[string]struct{})
|
||||
for _, n := range nodes {
|
||||
if n.Status.State != swarm.NodeStateDown {
|
||||
activeNodes[n.ID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
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]formatter.ServiceListInfo{}
|
||||
for _, service := range services {
|
||||
info[service.ID] = formatter.ServiceListInfo{}
|
||||
if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil {
|
||||
info[service.ID] = formatter.ServiceListInfo{
|
||||
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] = formatter.ServiceListInfo{
|
||||
Mode: "global",
|
||||
Replicas: fmt.Sprintf("%d/%d", running[service.ID], tasksNoShutdown[service.ID]),
|
||||
}
|
||||
}
|
||||
}
|
||||
return info
|
||||
}
|
||||
298
cli/command/service/logs.go
Normal file
298
cli/command/service/logs.go
Normal file
@ -0,0 +1,298 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/docker/cli/command/idresolver"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type logsOptions struct {
|
||||
noResolve bool
|
||||
noTrunc bool
|
||||
noTaskIDs bool
|
||||
follow bool
|
||||
since string
|
||||
timestamps bool
|
||||
tail string
|
||||
|
||||
target string
|
||||
}
|
||||
|
||||
// TODO(dperny) the whole CLI for this is kind of a mess IMHOIRL and it needs
|
||||
// to be refactored agressively. There may be changes to the implementation of
|
||||
// details, which will be need to be reflected in this code. The refactoring
|
||||
// should be put off until we make those changes, tho, because I think the
|
||||
// decisions made WRT details will impact the design of the CLI.
|
||||
func newLogsCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts logsOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "logs [OPTIONS] SERVICE|TASK",
|
||||
Short: "Fetch the logs of a service or task",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.target = args[0]
|
||||
return runLogs(dockerCli, &opts)
|
||||
},
|
||||
Tags: map[string]string{"version": "1.29"},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
// options specific to service logs
|
||||
flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names in output")
|
||||
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
|
||||
flags.BoolVar(&opts.noTaskIDs, "no-task-ids", false, "Do not include task IDs in output")
|
||||
// options identical to container logs
|
||||
flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output")
|
||||
flags.StringVar(&opts.since, "since", "", "Show logs since timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)")
|
||||
flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps")
|
||||
flags.StringVar(&opts.tail, "tail", "all", "Number of lines to show from the end of the logs")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
options := types.ContainerLogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Since: opts.since,
|
||||
Timestamps: opts.timestamps,
|
||||
Follow: opts.follow,
|
||||
Tail: opts.tail,
|
||||
Details: true,
|
||||
}
|
||||
|
||||
cli := dockerCli.Client()
|
||||
|
||||
var (
|
||||
maxLength = 1
|
||||
responseBody io.ReadCloser
|
||||
tty bool
|
||||
)
|
||||
|
||||
service, _, err := cli.ServiceInspectWithRaw(ctx, opts.target, types.ServiceInspectOptions{})
|
||||
if err != nil {
|
||||
// if it's any error other than service not found, it's Real
|
||||
if !client.IsErrServiceNotFound(err) {
|
||||
return err
|
||||
}
|
||||
task, _, err := cli.TaskInspectWithRaw(ctx, opts.target)
|
||||
tty = task.Spec.ContainerSpec.TTY
|
||||
// TODO(dperny) hot fix until we get a nice details system squared away,
|
||||
// ignores details (including task context) if we have a TTY log
|
||||
// if we don't do this, we'll vomit the huge context verbatim into the
|
||||
// TTY log lines and that's Undesirable.
|
||||
if tty {
|
||||
options.Details = false
|
||||
}
|
||||
|
||||
responseBody, err = cli.TaskLogs(ctx, opts.target, options)
|
||||
if err != nil {
|
||||
if client.IsErrTaskNotFound(err) {
|
||||
// if the task ALSO isn't found, rewrite the error to be clear
|
||||
// that we looked for services AND tasks
|
||||
err = fmt.Errorf("No such task or service")
|
||||
}
|
||||
return err
|
||||
}
|
||||
maxLength = getMaxLength(task.Slot)
|
||||
responseBody, err = cli.TaskLogs(ctx, opts.target, options)
|
||||
} else {
|
||||
tty = service.Spec.TaskTemplate.ContainerSpec.TTY
|
||||
// TODO(dperny) hot fix until we get a nice details system squared away,
|
||||
// ignores details (including task context) if we have a TTY log
|
||||
if tty {
|
||||
options.Details = false
|
||||
}
|
||||
|
||||
responseBody, err = cli.ServiceLogs(ctx, opts.target, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil {
|
||||
// if replicas are initialized, figure out if we need to pad them
|
||||
replicas := *service.Spec.Mode.Replicated.Replicas
|
||||
maxLength = getMaxLength(int(replicas))
|
||||
}
|
||||
}
|
||||
defer responseBody.Close()
|
||||
|
||||
if tty {
|
||||
_, err = io.Copy(dockerCli.Out(), responseBody)
|
||||
return err
|
||||
}
|
||||
|
||||
taskFormatter := newTaskFormatter(cli, opts, maxLength)
|
||||
|
||||
stdout := &logWriter{ctx: ctx, opts: opts, f: taskFormatter, w: dockerCli.Out()}
|
||||
stderr := &logWriter{ctx: ctx, opts: opts, f: taskFormatter, w: dockerCli.Err()}
|
||||
|
||||
// TODO(aluzzardi): Do an io.Copy for services with TTY enabled.
|
||||
_, err = stdcopy.StdCopy(stdout, stderr, responseBody)
|
||||
return err
|
||||
}
|
||||
|
||||
// getMaxLength gets the maximum length of the number in base 10
|
||||
func getMaxLength(i int) int {
|
||||
return len(strconv.FormatInt(int64(i), 10))
|
||||
}
|
||||
|
||||
type taskFormatter struct {
|
||||
client client.APIClient
|
||||
opts *logsOptions
|
||||
padding int
|
||||
|
||||
r *idresolver.IDResolver
|
||||
cache map[logContext]string
|
||||
}
|
||||
|
||||
func newTaskFormatter(client client.APIClient, opts *logsOptions, padding int) *taskFormatter {
|
||||
return &taskFormatter{
|
||||
client: client,
|
||||
opts: opts,
|
||||
padding: padding,
|
||||
r: idresolver.New(client, opts.noResolve),
|
||||
cache: make(map[logContext]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (f *taskFormatter) format(ctx context.Context, logCtx logContext) (string, error) {
|
||||
if cached, ok := f.cache[logCtx]; ok {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
nodeName, err := f.r.Resolve(ctx, swarm.Node{}, logCtx.nodeID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
serviceName, err := f.r.Resolve(ctx, swarm.Service{}, logCtx.serviceID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
task, _, err := f.client.TaskInspectWithRaw(ctx, logCtx.taskID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
taskName := fmt.Sprintf("%s.%d", serviceName, task.Slot)
|
||||
if !f.opts.noTaskIDs {
|
||||
if f.opts.noTrunc {
|
||||
taskName += fmt.Sprintf(".%s", task.ID)
|
||||
} else {
|
||||
taskName += fmt.Sprintf(".%s", stringid.TruncateID(task.ID))
|
||||
}
|
||||
}
|
||||
|
||||
padding := strings.Repeat(" ", f.padding-getMaxLength(task.Slot))
|
||||
formatted := fmt.Sprintf("%s@%s%s", taskName, nodeName, padding)
|
||||
f.cache[logCtx] = formatted
|
||||
return formatted, nil
|
||||
}
|
||||
|
||||
type logWriter struct {
|
||||
ctx context.Context
|
||||
opts *logsOptions
|
||||
f *taskFormatter
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func (lw *logWriter) Write(buf []byte) (int, error) {
|
||||
contextIndex := 0
|
||||
numParts := 2
|
||||
if lw.opts.timestamps {
|
||||
contextIndex++
|
||||
numParts++
|
||||
}
|
||||
|
||||
parts := bytes.SplitN(buf, []byte(" "), numParts)
|
||||
if len(parts) != numParts {
|
||||
return 0, errors.Errorf("invalid context in log message: %v", string(buf))
|
||||
}
|
||||
|
||||
logCtx, err := lw.parseContext(string(parts[contextIndex]))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
output := []byte{}
|
||||
for i, part := range parts {
|
||||
// First part doesn't get space separation.
|
||||
if i > 0 {
|
||||
output = append(output, []byte(" ")...)
|
||||
}
|
||||
|
||||
if i == contextIndex {
|
||||
formatted, err := lw.f.format(lw.ctx, logCtx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
output = append(output, []byte(fmt.Sprintf("%s |", formatted))...)
|
||||
} else {
|
||||
output = append(output, part...)
|
||||
}
|
||||
}
|
||||
_, err = lw.w.Write(output)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return len(buf), nil
|
||||
}
|
||||
|
||||
func (lw *logWriter) parseContext(input string) (logContext, error) {
|
||||
context := make(map[string]string)
|
||||
|
||||
components := strings.Split(input, ",")
|
||||
for _, component := range components {
|
||||
parts := strings.SplitN(component, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return logContext{}, errors.Errorf("invalid context: %s", input)
|
||||
}
|
||||
context[parts[0]] = parts[1]
|
||||
}
|
||||
|
||||
nodeID, ok := context["com.docker.swarm.node.id"]
|
||||
if !ok {
|
||||
return logContext{}, errors.Errorf("missing node id in context: %s", input)
|
||||
}
|
||||
|
||||
serviceID, ok := context["com.docker.swarm.service.id"]
|
||||
if !ok {
|
||||
return logContext{}, errors.Errorf("missing service id in context: %s", input)
|
||||
}
|
||||
|
||||
taskID, ok := context["com.docker.swarm.task.id"]
|
||||
if !ok {
|
||||
return logContext{}, errors.Errorf("missing task id in context: %s", input)
|
||||
}
|
||||
|
||||
return logContext{
|
||||
nodeID: nodeID,
|
||||
serviceID: serviceID,
|
||||
taskID: taskID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type logContext struct {
|
||||
nodeID string
|
||||
serviceID string
|
||||
taskID string
|
||||
}
|
||||
912
cli/command/service/opts.go
Normal file
912
cli/command/service/opts.go
Normal file
@ -0,0 +1,912 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/opts"
|
||||
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||
"github.com/docker/swarmkit/api"
|
||||
"github.com/docker/swarmkit/api/defaults"
|
||||
shlex "github.com/flynn-archive/go-shlex"
|
||||
gogotypes "github.com/gogo/protobuf/types"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type int64Value interface {
|
||||
Value() int64
|
||||
}
|
||||
|
||||
// PositiveDurationOpt is an option type for time.Duration that uses a pointer.
|
||||
// It bahave similarly to DurationOpt but only allows positive duration values.
|
||||
type PositiveDurationOpt struct {
|
||||
DurationOpt
|
||||
}
|
||||
|
||||
// Set a new value on the option. Setting a negative duration value will cause
|
||||
// an error to be returned.
|
||||
func (d *PositiveDurationOpt) Set(s string) error {
|
||||
err := d.DurationOpt.Set(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *d.DurationOpt.value < 0 {
|
||||
return errors.Errorf("duration cannot be negative")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DurationOpt is an option type for time.Duration that uses a pointer. This
|
||||
// allows us to get nil values outside, instead of defaulting to 0
|
||||
type DurationOpt struct {
|
||||
value *time.Duration
|
||||
}
|
||||
|
||||
// Set a new value on the option
|
||||
func (d *DurationOpt) Set(s string) error {
|
||||
v, err := time.ParseDuration(s)
|
||||
d.value = &v
|
||||
return err
|
||||
}
|
||||
|
||||
// Type returns the type of this option, which will be displayed in `--help` output
|
||||
func (d *DurationOpt) Type() string {
|
||||
return "duration"
|
||||
}
|
||||
|
||||
// String returns a string repr of this option
|
||||
func (d *DurationOpt) String() string {
|
||||
if d.value != nil {
|
||||
return d.value.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Value returns the time.Duration
|
||||
func (d *DurationOpt) Value() *time.Duration {
|
||||
return d.value
|
||||
}
|
||||
|
||||
// Uint64Opt represents a uint64.
|
||||
type Uint64Opt struct {
|
||||
value *uint64
|
||||
}
|
||||
|
||||
// Set a new value on the option
|
||||
func (i *Uint64Opt) Set(s string) error {
|
||||
v, err := strconv.ParseUint(s, 0, 64)
|
||||
i.value = &v
|
||||
return err
|
||||
}
|
||||
|
||||
// Type returns the type of this option, which will be displayed in `--help` output
|
||||
func (i *Uint64Opt) Type() string {
|
||||
return "uint"
|
||||
}
|
||||
|
||||
// String returns a string repr of this option
|
||||
func (i *Uint64Opt) String() string {
|
||||
if i.value != nil {
|
||||
return fmt.Sprintf("%v", *i.value)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Value returns the uint64
|
||||
func (i *Uint64Opt) Value() *uint64 {
|
||||
return i.value
|
||||
}
|
||||
|
||||
type floatValue float32
|
||||
|
||||
func (f *floatValue) Set(s string) error {
|
||||
v, err := strconv.ParseFloat(s, 32)
|
||||
*f = floatValue(v)
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *floatValue) Type() string {
|
||||
return "float"
|
||||
}
|
||||
|
||||
func (f *floatValue) String() string {
|
||||
return strconv.FormatFloat(float64(*f), 'g', -1, 32)
|
||||
}
|
||||
|
||||
func (f *floatValue) Value() float32 {
|
||||
return float32(*f)
|
||||
}
|
||||
|
||||
// placementPrefOpts holds a list of placement preferences.
|
||||
type placementPrefOpts struct {
|
||||
prefs []swarm.PlacementPreference
|
||||
strings []string
|
||||
}
|
||||
|
||||
func (opts *placementPrefOpts) String() string {
|
||||
if len(opts.strings) == 0 {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%v", opts.strings)
|
||||
}
|
||||
|
||||
// Set validates the input value and adds it to the internal slices.
|
||||
// Note: in the future strategies other than "spread", may be supported,
|
||||
// as well as additional comma-separated options.
|
||||
func (opts *placementPrefOpts) Set(value string) error {
|
||||
fields := strings.Split(value, "=")
|
||||
if len(fields) != 2 {
|
||||
return errors.New(`placement preference must be of the format "<strategy>=<arg>"`)
|
||||
}
|
||||
if fields[0] != "spread" {
|
||||
return errors.Errorf("unsupported placement preference %s (only spread is supported)", fields[0])
|
||||
}
|
||||
|
||||
opts.prefs = append(opts.prefs, swarm.PlacementPreference{
|
||||
Spread: &swarm.SpreadOver{
|
||||
SpreadDescriptor: fields[1],
|
||||
},
|
||||
})
|
||||
opts.strings = append(opts.strings, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Type returns a string name for this Option type
|
||||
func (opts *placementPrefOpts) Type() string {
|
||||
return "pref"
|
||||
}
|
||||
|
||||
// ShlexOpt is a flag Value which parses a string as a list of shell words
|
||||
type ShlexOpt []string
|
||||
|
||||
// Set the value
|
||||
func (s *ShlexOpt) Set(value string) error {
|
||||
valueSlice, err := shlex.Split(value)
|
||||
*s = ShlexOpt(valueSlice)
|
||||
return err
|
||||
}
|
||||
|
||||
// Type returns the tyep of the value
|
||||
func (s *ShlexOpt) Type() string {
|
||||
return "command"
|
||||
}
|
||||
|
||||
func (s *ShlexOpt) String() string {
|
||||
if len(*s) == 0 {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprint(*s)
|
||||
}
|
||||
|
||||
// Value returns the value as a string slice
|
||||
func (s *ShlexOpt) Value() []string {
|
||||
return []string(*s)
|
||||
}
|
||||
|
||||
type updateOptions struct {
|
||||
parallelism uint64
|
||||
delay time.Duration
|
||||
monitor time.Duration
|
||||
onFailure string
|
||||
maxFailureRatio floatValue
|
||||
order string
|
||||
}
|
||||
|
||||
func updateConfigFromDefaults(defaultUpdateConfig *api.UpdateConfig) *swarm.UpdateConfig {
|
||||
defaultFailureAction := strings.ToLower(api.UpdateConfig_FailureAction_name[int32(defaultUpdateConfig.FailureAction)])
|
||||
defaultMonitor, _ := gogotypes.DurationFromProto(defaultUpdateConfig.Monitor)
|
||||
return &swarm.UpdateConfig{
|
||||
Parallelism: defaultUpdateConfig.Parallelism,
|
||||
Delay: defaultUpdateConfig.Delay,
|
||||
Monitor: defaultMonitor,
|
||||
FailureAction: defaultFailureAction,
|
||||
MaxFailureRatio: defaultUpdateConfig.MaxFailureRatio,
|
||||
Order: defaultOrder(defaultUpdateConfig.Order),
|
||||
}
|
||||
}
|
||||
|
||||
func (opts updateOptions) updateConfig(flags *pflag.FlagSet) *swarm.UpdateConfig {
|
||||
if !anyChanged(flags, flagUpdateParallelism, flagUpdateDelay, flagUpdateMonitor, flagUpdateFailureAction, flagUpdateMaxFailureRatio) {
|
||||
return nil
|
||||
}
|
||||
|
||||
updateConfig := updateConfigFromDefaults(defaults.Service.Update)
|
||||
|
||||
if flags.Changed(flagUpdateParallelism) {
|
||||
updateConfig.Parallelism = opts.parallelism
|
||||
}
|
||||
if flags.Changed(flagUpdateDelay) {
|
||||
updateConfig.Delay = opts.delay
|
||||
}
|
||||
if flags.Changed(flagUpdateMonitor) {
|
||||
updateConfig.Monitor = opts.monitor
|
||||
}
|
||||
if flags.Changed(flagUpdateFailureAction) {
|
||||
updateConfig.FailureAction = opts.onFailure
|
||||
}
|
||||
if flags.Changed(flagUpdateMaxFailureRatio) {
|
||||
updateConfig.MaxFailureRatio = opts.maxFailureRatio.Value()
|
||||
}
|
||||
if flags.Changed(flagUpdateOrder) {
|
||||
updateConfig.Order = opts.order
|
||||
}
|
||||
|
||||
return updateConfig
|
||||
}
|
||||
|
||||
func (opts updateOptions) rollbackConfig(flags *pflag.FlagSet) *swarm.UpdateConfig {
|
||||
if !anyChanged(flags, flagRollbackParallelism, flagRollbackDelay, flagRollbackMonitor, flagRollbackFailureAction, flagRollbackMaxFailureRatio) {
|
||||
return nil
|
||||
}
|
||||
|
||||
updateConfig := updateConfigFromDefaults(defaults.Service.Rollback)
|
||||
|
||||
if flags.Changed(flagRollbackParallelism) {
|
||||
updateConfig.Parallelism = opts.parallelism
|
||||
}
|
||||
if flags.Changed(flagRollbackDelay) {
|
||||
updateConfig.Delay = opts.delay
|
||||
}
|
||||
if flags.Changed(flagRollbackMonitor) {
|
||||
updateConfig.Monitor = opts.monitor
|
||||
}
|
||||
if flags.Changed(flagRollbackFailureAction) {
|
||||
updateConfig.FailureAction = opts.onFailure
|
||||
}
|
||||
if flags.Changed(flagRollbackMaxFailureRatio) {
|
||||
updateConfig.MaxFailureRatio = opts.maxFailureRatio.Value()
|
||||
}
|
||||
if flags.Changed(flagRollbackOrder) {
|
||||
updateConfig.Order = opts.order
|
||||
}
|
||||
|
||||
return updateConfig
|
||||
}
|
||||
|
||||
type resourceOptions struct {
|
||||
limitCPU opts.NanoCPUs
|
||||
limitMemBytes opts.MemBytes
|
||||
resCPU opts.NanoCPUs
|
||||
resMemBytes opts.MemBytes
|
||||
}
|
||||
|
||||
func (r *resourceOptions) ToResourceRequirements() *swarm.ResourceRequirements {
|
||||
return &swarm.ResourceRequirements{
|
||||
Limits: &swarm.Resources{
|
||||
NanoCPUs: r.limitCPU.Value(),
|
||||
MemoryBytes: r.limitMemBytes.Value(),
|
||||
},
|
||||
Reservations: &swarm.Resources{
|
||||
NanoCPUs: r.resCPU.Value(),
|
||||
MemoryBytes: r.resMemBytes.Value(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type restartPolicyOptions struct {
|
||||
condition string
|
||||
delay DurationOpt
|
||||
maxAttempts Uint64Opt
|
||||
window DurationOpt
|
||||
}
|
||||
|
||||
func defaultRestartPolicy() *swarm.RestartPolicy {
|
||||
defaultMaxAttempts := defaults.Service.Task.Restart.MaxAttempts
|
||||
rp := &swarm.RestartPolicy{
|
||||
MaxAttempts: &defaultMaxAttempts,
|
||||
}
|
||||
|
||||
if defaults.Service.Task.Restart.Delay != nil {
|
||||
defaultRestartDelay, _ := gogotypes.DurationFromProto(defaults.Service.Task.Restart.Delay)
|
||||
rp.Delay = &defaultRestartDelay
|
||||
}
|
||||
if defaults.Service.Task.Restart.Window != nil {
|
||||
defaultRestartWindow, _ := gogotypes.DurationFromProto(defaults.Service.Task.Restart.Window)
|
||||
rp.Window = &defaultRestartWindow
|
||||
}
|
||||
rp.Condition = defaultRestartCondition()
|
||||
|
||||
return rp
|
||||
}
|
||||
|
||||
func defaultRestartCondition() swarm.RestartPolicyCondition {
|
||||
switch defaults.Service.Task.Restart.Condition {
|
||||
case api.RestartOnNone:
|
||||
return "none"
|
||||
case api.RestartOnFailure:
|
||||
return "on-failure"
|
||||
case api.RestartOnAny:
|
||||
return "any"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func defaultOrder(order api.UpdateConfig_UpdateOrder) string {
|
||||
switch order {
|
||||
case api.UpdateConfig_STOP_FIRST:
|
||||
return "stop-first"
|
||||
case api.UpdateConfig_START_FIRST:
|
||||
return "start-first"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (r *restartPolicyOptions) ToRestartPolicy(flags *pflag.FlagSet) *swarm.RestartPolicy {
|
||||
if !anyChanged(flags, flagRestartDelay, flagRestartMaxAttempts, flagRestartWindow, flagRestartCondition) {
|
||||
return nil
|
||||
}
|
||||
|
||||
restartPolicy := defaultRestartPolicy()
|
||||
|
||||
if flags.Changed(flagRestartDelay) {
|
||||
restartPolicy.Delay = r.delay.Value()
|
||||
}
|
||||
if flags.Changed(flagRestartCondition) {
|
||||
restartPolicy.Condition = swarm.RestartPolicyCondition(r.condition)
|
||||
}
|
||||
if flags.Changed(flagRestartMaxAttempts) {
|
||||
restartPolicy.MaxAttempts = r.maxAttempts.Value()
|
||||
}
|
||||
if flags.Changed(flagRestartWindow) {
|
||||
restartPolicy.Window = r.window.Value()
|
||||
}
|
||||
|
||||
return restartPolicy
|
||||
}
|
||||
|
||||
type credentialSpecOpt struct {
|
||||
value *swarm.CredentialSpec
|
||||
source string
|
||||
}
|
||||
|
||||
func (c *credentialSpecOpt) Set(value string) error {
|
||||
c.source = value
|
||||
c.value = &swarm.CredentialSpec{}
|
||||
switch {
|
||||
case strings.HasPrefix(value, "file://"):
|
||||
c.value.File = strings.TrimPrefix(value, "file://")
|
||||
case strings.HasPrefix(value, "registry://"):
|
||||
c.value.Registry = strings.TrimPrefix(value, "registry://")
|
||||
default:
|
||||
return errors.New("Invalid credential spec - value must be prefixed file:// or registry:// followed by a value")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *credentialSpecOpt) Type() string {
|
||||
return "credential-spec"
|
||||
}
|
||||
|
||||
func (c *credentialSpecOpt) String() string {
|
||||
return c.source
|
||||
}
|
||||
|
||||
func (c *credentialSpecOpt) Value() *swarm.CredentialSpec {
|
||||
return c.value
|
||||
}
|
||||
|
||||
func convertNetworks(ctx context.Context, apiClient client.NetworkAPIClient, networks []string) ([]swarm.NetworkAttachmentConfig, error) {
|
||||
nets := []swarm.NetworkAttachmentConfig{}
|
||||
for _, networkIDOrName := range networks {
|
||||
network, err := apiClient.NetworkInspect(ctx, networkIDOrName, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nets = append(nets, swarm.NetworkAttachmentConfig{Target: network.ID})
|
||||
}
|
||||
sort.Sort(byNetworkTarget(nets))
|
||||
return nets, nil
|
||||
}
|
||||
|
||||
type endpointOptions struct {
|
||||
mode string
|
||||
publishPorts opts.PortOpt
|
||||
}
|
||||
|
||||
func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec {
|
||||
return &swarm.EndpointSpec{
|
||||
Mode: swarm.ResolutionMode(strings.ToLower(e.mode)),
|
||||
Ports: e.publishPorts.Value(),
|
||||
}
|
||||
}
|
||||
|
||||
type logDriverOptions struct {
|
||||
name string
|
||||
opts opts.ListOpts
|
||||
}
|
||||
|
||||
func newLogDriverOptions() logDriverOptions {
|
||||
return logDriverOptions{opts: opts.NewListOpts(opts.ValidateEnv)}
|
||||
}
|
||||
|
||||
func (ldo *logDriverOptions) toLogDriver() *swarm.Driver {
|
||||
if ldo.name == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// set the log driver only if specified.
|
||||
return &swarm.Driver{
|
||||
Name: ldo.name,
|
||||
Options: runconfigopts.ConvertKVStringsToMap(ldo.opts.GetAll()),
|
||||
}
|
||||
}
|
||||
|
||||
type healthCheckOptions struct {
|
||||
cmd string
|
||||
interval PositiveDurationOpt
|
||||
timeout PositiveDurationOpt
|
||||
retries int
|
||||
startPeriod PositiveDurationOpt
|
||||
noHealthcheck bool
|
||||
}
|
||||
|
||||
func (opts *healthCheckOptions) toHealthConfig() (*container.HealthConfig, error) {
|
||||
var healthConfig *container.HealthConfig
|
||||
haveHealthSettings := opts.cmd != "" ||
|
||||
opts.interval.Value() != nil ||
|
||||
opts.timeout.Value() != nil ||
|
||||
opts.retries != 0
|
||||
if opts.noHealthcheck {
|
||||
if haveHealthSettings {
|
||||
return nil, errors.Errorf("--%s conflicts with --health-* options", flagNoHealthcheck)
|
||||
}
|
||||
healthConfig = &container.HealthConfig{Test: []string{"NONE"}}
|
||||
} else if haveHealthSettings {
|
||||
var test []string
|
||||
if opts.cmd != "" {
|
||||
test = []string{"CMD-SHELL", opts.cmd}
|
||||
}
|
||||
var interval, timeout, startPeriod time.Duration
|
||||
if ptr := opts.interval.Value(); ptr != nil {
|
||||
interval = *ptr
|
||||
}
|
||||
if ptr := opts.timeout.Value(); ptr != nil {
|
||||
timeout = *ptr
|
||||
}
|
||||
if ptr := opts.startPeriod.Value(); ptr != nil {
|
||||
startPeriod = *ptr
|
||||
}
|
||||
healthConfig = &container.HealthConfig{
|
||||
Test: test,
|
||||
Interval: interval,
|
||||
Timeout: timeout,
|
||||
Retries: opts.retries,
|
||||
StartPeriod: startPeriod,
|
||||
}
|
||||
}
|
||||
return healthConfig, nil
|
||||
}
|
||||
|
||||
// convertExtraHostsToSwarmHosts converts an array of extra hosts in cli
|
||||
// <host>:<ip>
|
||||
// into a swarmkit host format:
|
||||
// IP_address canonical_hostname [aliases...]
|
||||
// This assumes input value (<host>:<ip>) has already been validated
|
||||
func convertExtraHostsToSwarmHosts(extraHosts []string) []string {
|
||||
hosts := []string{}
|
||||
for _, extraHost := range extraHosts {
|
||||
parts := strings.SplitN(extraHost, ":", 2)
|
||||
hosts = append(hosts, fmt.Sprintf("%s %s", parts[1], parts[0]))
|
||||
}
|
||||
return hosts
|
||||
}
|
||||
|
||||
type serviceOptions struct {
|
||||
detach bool
|
||||
quiet bool
|
||||
|
||||
name string
|
||||
labels opts.ListOpts
|
||||
containerLabels opts.ListOpts
|
||||
image string
|
||||
entrypoint ShlexOpt
|
||||
args []string
|
||||
hostname string
|
||||
env opts.ListOpts
|
||||
envFile opts.ListOpts
|
||||
workdir string
|
||||
user string
|
||||
groups opts.ListOpts
|
||||
credentialSpec credentialSpecOpt
|
||||
stopSignal string
|
||||
tty bool
|
||||
readOnly bool
|
||||
mounts opts.MountOpt
|
||||
dns opts.ListOpts
|
||||
dnsSearch opts.ListOpts
|
||||
dnsOption opts.ListOpts
|
||||
hosts opts.ListOpts
|
||||
|
||||
resources resourceOptions
|
||||
stopGrace DurationOpt
|
||||
|
||||
replicas Uint64Opt
|
||||
mode string
|
||||
|
||||
restartPolicy restartPolicyOptions
|
||||
constraints opts.ListOpts
|
||||
placementPrefs placementPrefOpts
|
||||
update updateOptions
|
||||
rollback updateOptions
|
||||
networks opts.ListOpts
|
||||
endpoint endpointOptions
|
||||
|
||||
registryAuth bool
|
||||
|
||||
logDriver logDriverOptions
|
||||
|
||||
healthcheck healthCheckOptions
|
||||
secrets opts.SecretOpt
|
||||
}
|
||||
|
||||
func newServiceOptions() *serviceOptions {
|
||||
return &serviceOptions{
|
||||
labels: opts.NewListOpts(opts.ValidateEnv),
|
||||
constraints: opts.NewListOpts(nil),
|
||||
containerLabels: opts.NewListOpts(opts.ValidateEnv),
|
||||
env: opts.NewListOpts(opts.ValidateEnv),
|
||||
envFile: opts.NewListOpts(nil),
|
||||
groups: opts.NewListOpts(nil),
|
||||
logDriver: newLogDriverOptions(),
|
||||
dns: opts.NewListOpts(opts.ValidateIPAddress),
|
||||
dnsOption: opts.NewListOpts(nil),
|
||||
dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch),
|
||||
hosts: opts.NewListOpts(opts.ValidateExtraHost),
|
||||
networks: opts.NewListOpts(nil),
|
||||
}
|
||||
}
|
||||
|
||||
func (opts *serviceOptions) ToServiceMode() (swarm.ServiceMode, error) {
|
||||
serviceMode := swarm.ServiceMode{}
|
||||
switch opts.mode {
|
||||
case "global":
|
||||
if opts.replicas.Value() != nil {
|
||||
return serviceMode, errors.Errorf("replicas can only be used with replicated mode")
|
||||
}
|
||||
|
||||
serviceMode.Global = &swarm.GlobalService{}
|
||||
case "replicated":
|
||||
serviceMode.Replicated = &swarm.ReplicatedService{
|
||||
Replicas: opts.replicas.Value(),
|
||||
}
|
||||
default:
|
||||
return serviceMode, errors.Errorf("Unknown mode: %s, only replicated and global supported", opts.mode)
|
||||
}
|
||||
return serviceMode, nil
|
||||
}
|
||||
|
||||
func (opts *serviceOptions) ToStopGracePeriod(flags *pflag.FlagSet) *time.Duration {
|
||||
if flags.Changed(flagStopGracePeriod) {
|
||||
return opts.stopGrace.Value()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (opts *serviceOptions) ToService(ctx context.Context, apiClient client.APIClient, flags *pflag.FlagSet) (swarm.ServiceSpec, error) {
|
||||
var service swarm.ServiceSpec
|
||||
|
||||
envVariables, err := runconfigopts.ReadKVStrings(opts.envFile.GetAll(), opts.env.GetAll())
|
||||
if err != nil {
|
||||
return service, err
|
||||
}
|
||||
|
||||
currentEnv := make([]string, 0, len(envVariables))
|
||||
for _, env := range envVariables { // need to process each var, in order
|
||||
k := strings.SplitN(env, "=", 2)[0]
|
||||
for i, current := range currentEnv { // remove duplicates
|
||||
if current == env {
|
||||
continue // no update required, may hide this behind flag to preserve order of envVariables
|
||||
}
|
||||
if strings.HasPrefix(current, k+"=") {
|
||||
currentEnv = append(currentEnv[:i], currentEnv[i+1:]...)
|
||||
}
|
||||
}
|
||||
currentEnv = append(currentEnv, env)
|
||||
}
|
||||
|
||||
healthConfig, err := opts.healthcheck.toHealthConfig()
|
||||
if err != nil {
|
||||
return service, err
|
||||
}
|
||||
|
||||
serviceMode, err := opts.ToServiceMode()
|
||||
if err != nil {
|
||||
return service, err
|
||||
}
|
||||
|
||||
networks, err := convertNetworks(ctx, apiClient, opts.networks.GetAll())
|
||||
if err != nil {
|
||||
return service, err
|
||||
}
|
||||
|
||||
service = swarm.ServiceSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: opts.name,
|
||||
Labels: runconfigopts.ConvertKVStringsToMap(opts.labels.GetAll()),
|
||||
},
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
ContainerSpec: swarm.ContainerSpec{
|
||||
Image: opts.image,
|
||||
Args: opts.args,
|
||||
Command: opts.entrypoint.Value(),
|
||||
Env: currentEnv,
|
||||
Hostname: opts.hostname,
|
||||
Labels: runconfigopts.ConvertKVStringsToMap(opts.containerLabels.GetAll()),
|
||||
Dir: opts.workdir,
|
||||
User: opts.user,
|
||||
Groups: opts.groups.GetAll(),
|
||||
StopSignal: opts.stopSignal,
|
||||
TTY: opts.tty,
|
||||
ReadOnly: opts.readOnly,
|
||||
Mounts: opts.mounts.Value(),
|
||||
DNSConfig: &swarm.DNSConfig{
|
||||
Nameservers: opts.dns.GetAll(),
|
||||
Search: opts.dnsSearch.GetAll(),
|
||||
Options: opts.dnsOption.GetAll(),
|
||||
},
|
||||
Hosts: convertExtraHostsToSwarmHosts(opts.hosts.GetAll()),
|
||||
StopGracePeriod: opts.ToStopGracePeriod(flags),
|
||||
Secrets: nil,
|
||||
Healthcheck: healthConfig,
|
||||
},
|
||||
Networks: networks,
|
||||
Resources: opts.resources.ToResourceRequirements(),
|
||||
RestartPolicy: opts.restartPolicy.ToRestartPolicy(flags),
|
||||
Placement: &swarm.Placement{
|
||||
Constraints: opts.constraints.GetAll(),
|
||||
Preferences: opts.placementPrefs.prefs,
|
||||
},
|
||||
LogDriver: opts.logDriver.toLogDriver(),
|
||||
},
|
||||
Mode: serviceMode,
|
||||
UpdateConfig: opts.update.updateConfig(flags),
|
||||
RollbackConfig: opts.update.rollbackConfig(flags),
|
||||
EndpointSpec: opts.endpoint.ToEndpointSpec(),
|
||||
}
|
||||
|
||||
if opts.credentialSpec.Value() != nil {
|
||||
service.TaskTemplate.ContainerSpec.Privileges = &swarm.Privileges{
|
||||
CredentialSpec: opts.credentialSpec.Value(),
|
||||
}
|
||||
}
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
type flagDefaults map[string]interface{}
|
||||
|
||||
func (fd flagDefaults) getUint64(flagName string) uint64 {
|
||||
if val, ok := fd[flagName].(uint64); ok {
|
||||
return val
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (fd flagDefaults) getString(flagName string) string {
|
||||
if val, ok := fd[flagName].(string); ok {
|
||||
return val
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func buildServiceDefaultFlagMapping() flagDefaults {
|
||||
defaultFlagValues := make(map[string]interface{})
|
||||
|
||||
defaultFlagValues[flagStopGracePeriod], _ = gogotypes.DurationFromProto(defaults.Service.Task.GetContainer().StopGracePeriod)
|
||||
defaultFlagValues[flagRestartCondition] = `"` + defaultRestartCondition() + `"`
|
||||
defaultFlagValues[flagRestartDelay], _ = gogotypes.DurationFromProto(defaults.Service.Task.Restart.Delay)
|
||||
|
||||
if defaults.Service.Task.Restart.MaxAttempts != 0 {
|
||||
defaultFlagValues[flagRestartMaxAttempts] = defaults.Service.Task.Restart.MaxAttempts
|
||||
}
|
||||
|
||||
defaultRestartWindow, _ := gogotypes.DurationFromProto(defaults.Service.Task.Restart.Window)
|
||||
if defaultRestartWindow != 0 {
|
||||
defaultFlagValues[flagRestartWindow] = defaultRestartWindow
|
||||
}
|
||||
|
||||
defaultFlagValues[flagUpdateParallelism] = defaults.Service.Update.Parallelism
|
||||
defaultFlagValues[flagUpdateDelay] = defaults.Service.Update.Delay
|
||||
defaultFlagValues[flagUpdateMonitor], _ = gogotypes.DurationFromProto(defaults.Service.Update.Monitor)
|
||||
defaultFlagValues[flagUpdateFailureAction] = `"` + strings.ToLower(api.UpdateConfig_FailureAction_name[int32(defaults.Service.Update.FailureAction)]) + `"`
|
||||
defaultFlagValues[flagUpdateMaxFailureRatio] = defaults.Service.Update.MaxFailureRatio
|
||||
defaultFlagValues[flagUpdateOrder] = `"` + defaultOrder(defaults.Service.Update.Order) + `"`
|
||||
|
||||
defaultFlagValues[flagRollbackParallelism] = defaults.Service.Rollback.Parallelism
|
||||
defaultFlagValues[flagRollbackDelay] = defaults.Service.Rollback.Delay
|
||||
defaultFlagValues[flagRollbackMonitor], _ = gogotypes.DurationFromProto(defaults.Service.Rollback.Monitor)
|
||||
defaultFlagValues[flagRollbackFailureAction] = `"` + strings.ToLower(api.UpdateConfig_FailureAction_name[int32(defaults.Service.Rollback.FailureAction)]) + `"`
|
||||
defaultFlagValues[flagRollbackMaxFailureRatio] = defaults.Service.Rollback.MaxFailureRatio
|
||||
defaultFlagValues[flagRollbackOrder] = `"` + defaultOrder(defaults.Service.Rollback.Order) + `"`
|
||||
|
||||
defaultFlagValues[flagEndpointMode] = "vip"
|
||||
|
||||
return defaultFlagValues
|
||||
}
|
||||
|
||||
// addServiceFlags adds all flags that are common to both `create` and `update`.
|
||||
// Any flags that are not common are added separately in the individual command
|
||||
func addServiceFlags(flags *pflag.FlagSet, opts *serviceOptions, defaultFlagValues flagDefaults) {
|
||||
flagDesc := func(flagName string, desc string) string {
|
||||
if defaultValue, ok := defaultFlagValues[flagName]; ok {
|
||||
return fmt.Sprintf("%s (default %v)", desc, defaultValue)
|
||||
}
|
||||
return desc
|
||||
}
|
||||
|
||||
flags.BoolVarP(&opts.detach, "detach", "d", true, "Exit immediately instead of waiting for the service to converge")
|
||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress progress output")
|
||||
|
||||
flags.StringVarP(&opts.workdir, flagWorkdir, "w", "", "Working directory inside the container")
|
||||
flags.StringVarP(&opts.user, flagUser, "u", "", "Username or UID (format: <name|uid>[:<group|gid>])")
|
||||
flags.Var(&opts.credentialSpec, flagCredentialSpec, "Credential spec for managed service account (Windows only)")
|
||||
flags.SetAnnotation(flagCredentialSpec, "version", []string{"1.29"})
|
||||
flags.StringVar(&opts.hostname, flagHostname, "", "Container hostname")
|
||||
flags.SetAnnotation(flagHostname, "version", []string{"1.25"})
|
||||
flags.Var(&opts.entrypoint, flagEntrypoint, "Overwrite the default ENTRYPOINT of the image")
|
||||
|
||||
flags.Var(&opts.resources.limitCPU, flagLimitCPU, "Limit CPUs")
|
||||
flags.Var(&opts.resources.limitMemBytes, flagLimitMemory, "Limit Memory")
|
||||
flags.Var(&opts.resources.resCPU, flagReserveCPU, "Reserve CPUs")
|
||||
flags.Var(&opts.resources.resMemBytes, flagReserveMemory, "Reserve Memory")
|
||||
|
||||
flags.Var(&opts.stopGrace, flagStopGracePeriod, flagDesc(flagStopGracePeriod, "Time to wait before force killing a container (ns|us|ms|s|m|h)"))
|
||||
flags.Var(&opts.replicas, flagReplicas, "Number of tasks")
|
||||
|
||||
flags.StringVar(&opts.restartPolicy.condition, flagRestartCondition, "", flagDesc(flagRestartCondition, `Restart when condition is met ("none"|"on-failure"|"any")`))
|
||||
flags.Var(&opts.restartPolicy.delay, flagRestartDelay, flagDesc(flagRestartDelay, "Delay between restart attempts (ns|us|ms|s|m|h)"))
|
||||
flags.Var(&opts.restartPolicy.maxAttempts, flagRestartMaxAttempts, flagDesc(flagRestartMaxAttempts, "Maximum number of restarts before giving up"))
|
||||
|
||||
flags.Var(&opts.restartPolicy.window, flagRestartWindow, flagDesc(flagRestartWindow, "Window used to evaluate the restart policy (ns|us|ms|s|m|h)"))
|
||||
|
||||
flags.Uint64Var(&opts.update.parallelism, flagUpdateParallelism, defaultFlagValues.getUint64(flagUpdateParallelism), "Maximum number of tasks updated simultaneously (0 to update all at once)")
|
||||
flags.DurationVar(&opts.update.delay, flagUpdateDelay, 0, flagDesc(flagUpdateDelay, "Delay between updates (ns|us|ms|s|m|h)"))
|
||||
flags.DurationVar(&opts.update.monitor, flagUpdateMonitor, 0, flagDesc(flagUpdateMonitor, "Duration after each task update to monitor for failure (ns|us|ms|s|m|h)"))
|
||||
flags.SetAnnotation(flagUpdateMonitor, "version", []string{"1.25"})
|
||||
flags.StringVar(&opts.update.onFailure, flagUpdateFailureAction, "", flagDesc(flagUpdateFailureAction, `Action on update failure ("pause"|"continue"|"rollback")`))
|
||||
flags.Var(&opts.update.maxFailureRatio, flagUpdateMaxFailureRatio, flagDesc(flagUpdateMaxFailureRatio, "Failure rate to tolerate during an update"))
|
||||
flags.SetAnnotation(flagUpdateMaxFailureRatio, "version", []string{"1.25"})
|
||||
flags.StringVar(&opts.update.order, flagUpdateOrder, "", flagDesc(flagUpdateOrder, `Update order ("start-first"|"stop-first")`))
|
||||
flags.SetAnnotation(flagUpdateOrder, "version", []string{"1.29"})
|
||||
|
||||
flags.Uint64Var(&opts.rollback.parallelism, flagRollbackParallelism, defaultFlagValues.getUint64(flagRollbackParallelism), "Maximum number of tasks rolled back simultaneously (0 to roll back all at once)")
|
||||
flags.SetAnnotation(flagRollbackParallelism, "version", []string{"1.28"})
|
||||
flags.DurationVar(&opts.rollback.delay, flagRollbackDelay, 0, flagDesc(flagRollbackDelay, "Delay between task rollbacks (ns|us|ms|s|m|h)"))
|
||||
flags.SetAnnotation(flagRollbackDelay, "version", []string{"1.28"})
|
||||
flags.DurationVar(&opts.rollback.monitor, flagRollbackMonitor, 0, flagDesc(flagRollbackMonitor, "Duration after each task rollback to monitor for failure (ns|us|ms|s|m|h)"))
|
||||
flags.SetAnnotation(flagRollbackMonitor, "version", []string{"1.28"})
|
||||
flags.StringVar(&opts.rollback.onFailure, flagRollbackFailureAction, "", flagDesc(flagRollbackFailureAction, `Action on rollback failure ("pause"|"continue")`))
|
||||
flags.SetAnnotation(flagRollbackFailureAction, "version", []string{"1.28"})
|
||||
flags.Var(&opts.rollback.maxFailureRatio, flagRollbackMaxFailureRatio, flagDesc(flagRollbackMaxFailureRatio, "Failure rate to tolerate during a rollback"))
|
||||
flags.SetAnnotation(flagRollbackMaxFailureRatio, "version", []string{"1.28"})
|
||||
flags.StringVar(&opts.rollback.order, flagRollbackOrder, "", flagDesc(flagRollbackOrder, `Rollback order ("start-first"|"stop-first")`))
|
||||
flags.SetAnnotation(flagRollbackOrder, "version", []string{"1.29"})
|
||||
|
||||
flags.StringVar(&opts.endpoint.mode, flagEndpointMode, defaultFlagValues.getString(flagEndpointMode), "Endpoint mode (vip or dnsrr)")
|
||||
|
||||
flags.BoolVar(&opts.registryAuth, flagRegistryAuth, false, "Send registry authentication details to swarm agents")
|
||||
|
||||
flags.StringVar(&opts.logDriver.name, flagLogDriver, "", "Logging driver for service")
|
||||
flags.Var(&opts.logDriver.opts, flagLogOpt, "Logging driver options")
|
||||
|
||||
flags.StringVar(&opts.healthcheck.cmd, flagHealthCmd, "", "Command to run to check health")
|
||||
flags.SetAnnotation(flagHealthCmd, "version", []string{"1.25"})
|
||||
flags.Var(&opts.healthcheck.interval, flagHealthInterval, "Time between running the check (ns|us|ms|s|m|h)")
|
||||
flags.SetAnnotation(flagHealthInterval, "version", []string{"1.25"})
|
||||
flags.Var(&opts.healthcheck.timeout, flagHealthTimeout, "Maximum time to allow one check to run (ns|us|ms|s|m|h)")
|
||||
flags.SetAnnotation(flagHealthTimeout, "version", []string{"1.25"})
|
||||
flags.IntVar(&opts.healthcheck.retries, flagHealthRetries, 0, "Consecutive failures needed to report unhealthy")
|
||||
flags.SetAnnotation(flagHealthRetries, "version", []string{"1.25"})
|
||||
flags.Var(&opts.healthcheck.startPeriod, flagHealthStartPeriod, "Start period for the container to initialize before counting retries towards unstable (ns|us|ms|s|m|h)")
|
||||
flags.SetAnnotation(flagHealthStartPeriod, "version", []string{"1.29"})
|
||||
flags.BoolVar(&opts.healthcheck.noHealthcheck, flagNoHealthcheck, false, "Disable any container-specified HEALTHCHECK")
|
||||
flags.SetAnnotation(flagNoHealthcheck, "version", []string{"1.25"})
|
||||
|
||||
flags.BoolVarP(&opts.tty, flagTTY, "t", false, "Allocate a pseudo-TTY")
|
||||
flags.SetAnnotation(flagTTY, "version", []string{"1.25"})
|
||||
|
||||
flags.BoolVar(&opts.readOnly, flagReadOnly, false, "Mount the container's root filesystem as read only")
|
||||
flags.SetAnnotation(flagReadOnly, "version", []string{"1.28"})
|
||||
|
||||
flags.StringVar(&opts.stopSignal, flagStopSignal, "", "Signal to stop the container")
|
||||
flags.SetAnnotation(flagStopSignal, "version", []string{"1.28"})
|
||||
}
|
||||
|
||||
const (
|
||||
flagCredentialSpec = "credential-spec"
|
||||
flagPlacementPref = "placement-pref"
|
||||
flagPlacementPrefAdd = "placement-pref-add"
|
||||
flagPlacementPrefRemove = "placement-pref-rm"
|
||||
flagConstraint = "constraint"
|
||||
flagConstraintRemove = "constraint-rm"
|
||||
flagConstraintAdd = "constraint-add"
|
||||
flagContainerLabel = "container-label"
|
||||
flagContainerLabelRemove = "container-label-rm"
|
||||
flagContainerLabelAdd = "container-label-add"
|
||||
flagDNS = "dns"
|
||||
flagDNSRemove = "dns-rm"
|
||||
flagDNSAdd = "dns-add"
|
||||
flagDNSOption = "dns-option"
|
||||
flagDNSOptionRemove = "dns-option-rm"
|
||||
flagDNSOptionAdd = "dns-option-add"
|
||||
flagDNSSearch = "dns-search"
|
||||
flagDNSSearchRemove = "dns-search-rm"
|
||||
flagDNSSearchAdd = "dns-search-add"
|
||||
flagEndpointMode = "endpoint-mode"
|
||||
flagEntrypoint = "entrypoint"
|
||||
flagHost = "host"
|
||||
flagHostAdd = "host-add"
|
||||
flagHostRemove = "host-rm"
|
||||
flagHostname = "hostname"
|
||||
flagEnv = "env"
|
||||
flagEnvFile = "env-file"
|
||||
flagEnvRemove = "env-rm"
|
||||
flagEnvAdd = "env-add"
|
||||
flagGroup = "group"
|
||||
flagGroupAdd = "group-add"
|
||||
flagGroupRemove = "group-rm"
|
||||
flagLabel = "label"
|
||||
flagLabelRemove = "label-rm"
|
||||
flagLabelAdd = "label-add"
|
||||
flagLimitCPU = "limit-cpu"
|
||||
flagLimitMemory = "limit-memory"
|
||||
flagMode = "mode"
|
||||
flagMount = "mount"
|
||||
flagMountRemove = "mount-rm"
|
||||
flagMountAdd = "mount-add"
|
||||
flagName = "name"
|
||||
flagNetwork = "network"
|
||||
flagNetworkAdd = "network-add"
|
||||
flagNetworkRemove = "network-rm"
|
||||
flagPublish = "publish"
|
||||
flagPublishRemove = "publish-rm"
|
||||
flagPublishAdd = "publish-add"
|
||||
flagReadOnly = "read-only"
|
||||
flagReplicas = "replicas"
|
||||
flagReserveCPU = "reserve-cpu"
|
||||
flagReserveMemory = "reserve-memory"
|
||||
flagRestartCondition = "restart-condition"
|
||||
flagRestartDelay = "restart-delay"
|
||||
flagRestartMaxAttempts = "restart-max-attempts"
|
||||
flagRestartWindow = "restart-window"
|
||||
flagRollbackDelay = "rollback-delay"
|
||||
flagRollbackFailureAction = "rollback-failure-action"
|
||||
flagRollbackMaxFailureRatio = "rollback-max-failure-ratio"
|
||||
flagRollbackMonitor = "rollback-monitor"
|
||||
flagRollbackOrder = "rollback-order"
|
||||
flagRollbackParallelism = "rollback-parallelism"
|
||||
flagStopGracePeriod = "stop-grace-period"
|
||||
flagStopSignal = "stop-signal"
|
||||
flagTTY = "tty"
|
||||
flagUpdateDelay = "update-delay"
|
||||
flagUpdateFailureAction = "update-failure-action"
|
||||
flagUpdateMaxFailureRatio = "update-max-failure-ratio"
|
||||
flagUpdateMonitor = "update-monitor"
|
||||
flagUpdateOrder = "update-order"
|
||||
flagUpdateParallelism = "update-parallelism"
|
||||
flagUser = "user"
|
||||
flagWorkdir = "workdir"
|
||||
flagRegistryAuth = "with-registry-auth"
|
||||
flagLogDriver = "log-driver"
|
||||
flagLogOpt = "log-opt"
|
||||
flagHealthCmd = "health-cmd"
|
||||
flagHealthInterval = "health-interval"
|
||||
flagHealthRetries = "health-retries"
|
||||
flagHealthTimeout = "health-timeout"
|
||||
flagHealthStartPeriod = "health-start-period"
|
||||
flagNoHealthcheck = "no-healthcheck"
|
||||
flagSecret = "secret"
|
||||
flagSecretAdd = "secret-add"
|
||||
flagSecretRemove = "secret-rm"
|
||||
)
|
||||
108
cli/command/service/opts_test.go
Normal file
108
cli/command/service/opts_test.go
Normal file
@ -0,0 +1,108 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/opts"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMemBytesString(t *testing.T) {
|
||||
var mem opts.MemBytes = 1048576
|
||||
assert.Equal(t, "1MiB", mem.String())
|
||||
}
|
||||
|
||||
func TestMemBytesSetAndValue(t *testing.T) {
|
||||
var mem opts.MemBytes
|
||||
assert.NoError(t, mem.Set("5kb"))
|
||||
assert.Equal(t, int64(5120), mem.Value())
|
||||
}
|
||||
|
||||
func TestNanoCPUsString(t *testing.T) {
|
||||
var cpus opts.NanoCPUs = 6100000000
|
||||
assert.Equal(t, "6.100", cpus.String())
|
||||
}
|
||||
|
||||
func TestNanoCPUsSetAndValue(t *testing.T) {
|
||||
var cpus opts.NanoCPUs
|
||||
assert.NoError(t, cpus.Set("0.35"))
|
||||
assert.Equal(t, int64(350000000), cpus.Value())
|
||||
}
|
||||
|
||||
func TestDurationOptString(t *testing.T) {
|
||||
dur := time.Duration(300 * 10e8)
|
||||
duration := DurationOpt{value: &dur}
|
||||
assert.Equal(t, "5m0s", duration.String())
|
||||
}
|
||||
|
||||
func TestDurationOptSetAndValue(t *testing.T) {
|
||||
var duration DurationOpt
|
||||
assert.NoError(t, duration.Set("300s"))
|
||||
assert.Equal(t, time.Duration(300*10e8), *duration.Value())
|
||||
assert.NoError(t, duration.Set("-300s"))
|
||||
assert.Equal(t, time.Duration(-300*10e8), *duration.Value())
|
||||
}
|
||||
|
||||
func TestPositiveDurationOptSetAndValue(t *testing.T) {
|
||||
var duration PositiveDurationOpt
|
||||
assert.NoError(t, duration.Set("300s"))
|
||||
assert.Equal(t, time.Duration(300*10e8), *duration.Value())
|
||||
assert.EqualError(t, duration.Set("-300s"), "duration cannot be negative")
|
||||
}
|
||||
|
||||
func TestUint64OptString(t *testing.T) {
|
||||
value := uint64(2345678)
|
||||
opt := Uint64Opt{value: &value}
|
||||
assert.Equal(t, "2345678", opt.String())
|
||||
|
||||
opt = Uint64Opt{}
|
||||
assert.Equal(t, "", opt.String())
|
||||
}
|
||||
|
||||
func TestUint64OptSetAndValue(t *testing.T) {
|
||||
var opt Uint64Opt
|
||||
assert.NoError(t, opt.Set("14445"))
|
||||
assert.Equal(t, uint64(14445), *opt.Value())
|
||||
}
|
||||
|
||||
func TestHealthCheckOptionsToHealthConfig(t *testing.T) {
|
||||
dur := time.Second
|
||||
opt := healthCheckOptions{
|
||||
cmd: "curl",
|
||||
interval: PositiveDurationOpt{DurationOpt{value: &dur}},
|
||||
timeout: PositiveDurationOpt{DurationOpt{value: &dur}},
|
||||
startPeriod: PositiveDurationOpt{DurationOpt{value: &dur}},
|
||||
retries: 10,
|
||||
}
|
||||
config, err := opt.toHealthConfig()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, &container.HealthConfig{
|
||||
Test: []string{"CMD-SHELL", "curl"},
|
||||
Interval: time.Second,
|
||||
Timeout: time.Second,
|
||||
StartPeriod: time.Second,
|
||||
Retries: 10,
|
||||
}, config)
|
||||
}
|
||||
|
||||
func TestHealthCheckOptionsToHealthConfigNoHealthcheck(t *testing.T) {
|
||||
opt := healthCheckOptions{
|
||||
noHealthcheck: true,
|
||||
}
|
||||
config, err := opt.toHealthConfig()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, &container.HealthConfig{
|
||||
Test: []string{"NONE"},
|
||||
}, config)
|
||||
}
|
||||
|
||||
func TestHealthCheckOptionsToHealthConfigConflict(t *testing.T) {
|
||||
opt := healthCheckOptions{
|
||||
cmd: "curl",
|
||||
noHealthcheck: true,
|
||||
}
|
||||
_, err := opt.toHealthConfig()
|
||||
assert.EqualError(t, err, "--no-healthcheck conflicts with --health-* options")
|
||||
}
|
||||
59
cli/command/service/parse.go
Normal file
59
cli/command/service/parse.go
Normal file
@ -0,0 +1,59 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
swarmtypes "github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// ParseSecrets retrieves the secrets with the requested names and fills
|
||||
// secret IDs into the secret references.
|
||||
func ParseSecrets(client client.SecretAPIClient, requestedSecrets []*swarmtypes.SecretReference) ([]*swarmtypes.SecretReference, error) {
|
||||
secretRefs := make(map[string]*swarmtypes.SecretReference)
|
||||
ctx := context.Background()
|
||||
|
||||
for _, secret := range requestedSecrets {
|
||||
if _, exists := secretRefs[secret.File.Name]; exists {
|
||||
return nil, errors.Errorf("duplicate secret target for %s not allowed", secret.SecretName)
|
||||
}
|
||||
secretRef := new(swarmtypes.SecretReference)
|
||||
*secretRef = *secret
|
||||
secretRefs[secret.File.Name] = secretRef
|
||||
}
|
||||
|
||||
args := filters.NewArgs()
|
||||
for _, s := range secretRefs {
|
||||
args.Add("name", s.SecretName)
|
||||
}
|
||||
|
||||
secrets, err := client.SecretList(ctx, types.SecretListOptions{
|
||||
Filters: args,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
foundSecrets := make(map[string]string)
|
||||
for _, secret := range secrets {
|
||||
foundSecrets[secret.Spec.Annotations.Name] = secret.ID
|
||||
}
|
||||
|
||||
addedSecrets := []*swarmtypes.SecretReference{}
|
||||
|
||||
for _, ref := range secretRefs {
|
||||
id, ok := foundSecrets[ref.SecretName]
|
||||
if !ok {
|
||||
return nil, errors.Errorf("secret not found: %s", ref.SecretName)
|
||||
}
|
||||
|
||||
// set the id for the ref to properly assign in swarm
|
||||
// since swarm needs the ID instead of the name
|
||||
ref.SecretID = id
|
||||
addedSecrets = append(addedSecrets, ref)
|
||||
}
|
||||
|
||||
return addedSecrets, nil
|
||||
}
|
||||
409
cli/command/service/progress/progress.go
Normal file
409
cli/command/service/progress/progress.go
Normal file
@ -0,0 +1,409 @@
|
||||
package progress
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/progress"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
var (
|
||||
numberedStates = map[swarm.TaskState]int64{
|
||||
swarm.TaskStateNew: 1,
|
||||
swarm.TaskStateAllocated: 2,
|
||||
swarm.TaskStatePending: 3,
|
||||
swarm.TaskStateAssigned: 4,
|
||||
swarm.TaskStateAccepted: 5,
|
||||
swarm.TaskStatePreparing: 6,
|
||||
swarm.TaskStateReady: 7,
|
||||
swarm.TaskStateStarting: 8,
|
||||
swarm.TaskStateRunning: 9,
|
||||
}
|
||||
|
||||
longestState int
|
||||
)
|
||||
|
||||
const (
|
||||
maxProgress = 9
|
||||
maxProgressBars = 20
|
||||
)
|
||||
|
||||
type progressUpdater interface {
|
||||
update(service swarm.Service, tasks []swarm.Task, activeNodes map[string]swarm.Node, rollback bool) (bool, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
for state := range numberedStates {
|
||||
if len(state) > longestState {
|
||||
longestState = len(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stateToProgress(state swarm.TaskState, rollback bool) int64 {
|
||||
if !rollback {
|
||||
return numberedStates[state]
|
||||
}
|
||||
return int64(len(numberedStates)) - numberedStates[state]
|
||||
}
|
||||
|
||||
// ServiceProgress outputs progress information for convergence of a service.
|
||||
func ServiceProgress(ctx context.Context, client client.APIClient, serviceID string, progressWriter io.WriteCloser) error {
|
||||
defer progressWriter.Close()
|
||||
|
||||
progressOut := streamformatter.NewJSONStreamFormatter().NewProgressOutput(progressWriter, false)
|
||||
|
||||
sigint := make(chan os.Signal, 1)
|
||||
signal.Notify(sigint, os.Interrupt)
|
||||
defer signal.Stop(sigint)
|
||||
|
||||
taskFilter := filters.NewArgs()
|
||||
taskFilter.Add("service", serviceID)
|
||||
taskFilter.Add("_up-to-date", "true")
|
||||
|
||||
getUpToDateTasks := func() ([]swarm.Task, error) {
|
||||
return client.TaskList(ctx, types.TaskListOptions{Filters: taskFilter})
|
||||
}
|
||||
|
||||
var (
|
||||
updater progressUpdater
|
||||
converged bool
|
||||
convergedAt time.Time
|
||||
monitor = 5 * time.Second
|
||||
rollback bool
|
||||
)
|
||||
|
||||
for {
|
||||
service, _, err := client.ServiceInspectWithRaw(ctx, serviceID, types.ServiceInspectOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if service.Spec.UpdateConfig != nil && service.Spec.UpdateConfig.Monitor != 0 {
|
||||
monitor = service.Spec.UpdateConfig.Monitor
|
||||
}
|
||||
|
||||
if updater == nil {
|
||||
updater, err = initializeUpdater(service, progressOut)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if service.UpdateStatus != nil {
|
||||
switch service.UpdateStatus.State {
|
||||
case swarm.UpdateStateUpdating:
|
||||
rollback = false
|
||||
case swarm.UpdateStateCompleted:
|
||||
if !converged {
|
||||
return nil
|
||||
}
|
||||
case swarm.UpdateStatePaused:
|
||||
return fmt.Errorf("service update paused: %s", service.UpdateStatus.Message)
|
||||
case swarm.UpdateStateRollbackStarted:
|
||||
if !rollback && service.UpdateStatus.Message != "" {
|
||||
progressOut.WriteProgress(progress.Progress{
|
||||
ID: "rollback",
|
||||
Action: service.UpdateStatus.Message,
|
||||
})
|
||||
}
|
||||
rollback = true
|
||||
case swarm.UpdateStateRollbackPaused:
|
||||
return fmt.Errorf("service rollback paused: %s", service.UpdateStatus.Message)
|
||||
case swarm.UpdateStateRollbackCompleted:
|
||||
if !converged {
|
||||
return fmt.Errorf("service rolled back: %s", service.UpdateStatus.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
if converged && time.Since(convergedAt) >= monitor {
|
||||
return nil
|
||||
}
|
||||
|
||||
tasks, err := getUpToDateTasks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
activeNodes, err := getActiveNodes(ctx, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
converged, err = updater.update(service, tasks, activeNodes, rollback)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if converged {
|
||||
if convergedAt.IsZero() {
|
||||
convergedAt = time.Now()
|
||||
}
|
||||
wait := monitor - time.Since(convergedAt)
|
||||
if wait >= 0 {
|
||||
progressOut.WriteProgress(progress.Progress{
|
||||
// Ideally this would have no ID, but
|
||||
// the progress rendering code behaves
|
||||
// poorly on an "action" with no ID. It
|
||||
// returns the cursor to the beginning
|
||||
// of the line, so the first character
|
||||
// may be difficult to read. Then the
|
||||
// output is overwritten by the shell
|
||||
// prompt when the command finishes.
|
||||
ID: "verify",
|
||||
Action: fmt.Sprintf("Waiting %d seconds to verify that tasks are stable...", wait/time.Second+1),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if !convergedAt.IsZero() {
|
||||
progressOut.WriteProgress(progress.Progress{
|
||||
ID: "verify",
|
||||
Action: "Detected task failure",
|
||||
})
|
||||
}
|
||||
convergedAt = time.Time{}
|
||||
}
|
||||
|
||||
select {
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
case <-sigint:
|
||||
if !converged {
|
||||
progress.Message(progressOut, "", "Operation continuing in background.")
|
||||
progress.Messagef(progressOut, "", "Use `docker service ps %s` to check progress.", serviceID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getActiveNodes(ctx context.Context, client client.APIClient) (map[string]swarm.Node, error) {
|
||||
nodes, err := client.NodeList(ctx, types.NodeListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
activeNodes := make(map[string]swarm.Node)
|
||||
for _, n := range nodes {
|
||||
if n.Status.State != swarm.NodeStateDown {
|
||||
activeNodes[n.ID] = n
|
||||
}
|
||||
}
|
||||
return activeNodes, nil
|
||||
}
|
||||
|
||||
func initializeUpdater(service swarm.Service, progressOut progress.Output) (progressUpdater, error) {
|
||||
if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil {
|
||||
return &replicatedProgressUpdater{
|
||||
progressOut: progressOut,
|
||||
}, nil
|
||||
}
|
||||
if service.Spec.Mode.Global != nil {
|
||||
return &globalProgressUpdater{
|
||||
progressOut: progressOut,
|
||||
}, nil
|
||||
}
|
||||
return nil, errors.New("unrecognized service mode")
|
||||
}
|
||||
|
||||
func writeOverallProgress(progressOut progress.Output, numerator, denominator int, rollback bool) {
|
||||
if rollback {
|
||||
progressOut.WriteProgress(progress.Progress{
|
||||
ID: "overall progress",
|
||||
Action: fmt.Sprintf("rolling back update: %d out of %d tasks", numerator, denominator),
|
||||
})
|
||||
return
|
||||
}
|
||||
progressOut.WriteProgress(progress.Progress{
|
||||
ID: "overall progress",
|
||||
Action: fmt.Sprintf("%d out of %d tasks", numerator, denominator),
|
||||
})
|
||||
}
|
||||
|
||||
type replicatedProgressUpdater struct {
|
||||
progressOut progress.Output
|
||||
|
||||
// used for maping slots to a contiguous space
|
||||
// this also causes progress bars to appear in order
|
||||
slotMap map[int]int
|
||||
|
||||
initialized bool
|
||||
done bool
|
||||
}
|
||||
|
||||
func (u *replicatedProgressUpdater) update(service swarm.Service, tasks []swarm.Task, activeNodes map[string]swarm.Node, rollback bool) (bool, error) {
|
||||
if service.Spec.Mode.Replicated == nil || service.Spec.Mode.Replicated.Replicas == nil {
|
||||
return false, errors.New("no replica count")
|
||||
}
|
||||
replicas := *service.Spec.Mode.Replicated.Replicas
|
||||
|
||||
if !u.initialized {
|
||||
u.slotMap = make(map[int]int)
|
||||
|
||||
// Draw progress bars in order
|
||||
writeOverallProgress(u.progressOut, 0, int(replicas), rollback)
|
||||
|
||||
if replicas <= maxProgressBars {
|
||||
for i := uint64(1); i <= replicas; i++ {
|
||||
progress.Update(u.progressOut, fmt.Sprintf("%d/%d", i, replicas), " ")
|
||||
}
|
||||
}
|
||||
u.initialized = true
|
||||
}
|
||||
|
||||
// If there are multiple tasks with the same slot number, favor the one
|
||||
// with the *lowest* desired state. This can happen in restart
|
||||
// scenarios.
|
||||
tasksBySlot := make(map[int]swarm.Task)
|
||||
for _, task := range tasks {
|
||||
if numberedStates[task.DesiredState] == 0 {
|
||||
continue
|
||||
}
|
||||
if existingTask, ok := tasksBySlot[task.Slot]; ok {
|
||||
if numberedStates[existingTask.DesiredState] <= numberedStates[task.DesiredState] {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if _, nodeActive := activeNodes[task.NodeID]; nodeActive {
|
||||
tasksBySlot[task.Slot] = task
|
||||
}
|
||||
}
|
||||
|
||||
// If we had reached a converged state, check if we are still converged.
|
||||
if u.done {
|
||||
for _, task := range tasksBySlot {
|
||||
if task.Status.State != swarm.TaskStateRunning {
|
||||
u.done = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
running := uint64(0)
|
||||
|
||||
for _, task := range tasksBySlot {
|
||||
mappedSlot := u.slotMap[task.Slot]
|
||||
if mappedSlot == 0 {
|
||||
mappedSlot = len(u.slotMap) + 1
|
||||
u.slotMap[task.Slot] = mappedSlot
|
||||
}
|
||||
|
||||
if !u.done && replicas <= maxProgressBars && uint64(mappedSlot) <= replicas {
|
||||
u.progressOut.WriteProgress(progress.Progress{
|
||||
ID: fmt.Sprintf("%d/%d", mappedSlot, replicas),
|
||||
Action: fmt.Sprintf("%-[1]*s", longestState, task.Status.State),
|
||||
Current: stateToProgress(task.Status.State, rollback),
|
||||
Total: maxProgress,
|
||||
HideCounts: true,
|
||||
})
|
||||
}
|
||||
if task.Status.State == swarm.TaskStateRunning {
|
||||
running++
|
||||
}
|
||||
}
|
||||
|
||||
if !u.done {
|
||||
writeOverallProgress(u.progressOut, int(running), int(replicas), rollback)
|
||||
|
||||
if running == replicas {
|
||||
u.done = true
|
||||
}
|
||||
}
|
||||
|
||||
return running == replicas, nil
|
||||
}
|
||||
|
||||
type globalProgressUpdater struct {
|
||||
progressOut progress.Output
|
||||
|
||||
initialized bool
|
||||
done bool
|
||||
}
|
||||
|
||||
func (u *globalProgressUpdater) update(service swarm.Service, tasks []swarm.Task, activeNodes map[string]swarm.Node, rollback bool) (bool, error) {
|
||||
// If there are multiple tasks with the same node ID, favor the one
|
||||
// with the *lowest* desired state. This can happen in restart
|
||||
// scenarios.
|
||||
tasksByNode := make(map[string]swarm.Task)
|
||||
for _, task := range tasks {
|
||||
if numberedStates[task.DesiredState] == 0 {
|
||||
continue
|
||||
}
|
||||
if existingTask, ok := tasksByNode[task.NodeID]; ok {
|
||||
if numberedStates[existingTask.DesiredState] <= numberedStates[task.DesiredState] {
|
||||
continue
|
||||
}
|
||||
}
|
||||
tasksByNode[task.NodeID] = task
|
||||
}
|
||||
|
||||
// We don't have perfect knowledge of how many nodes meet the
|
||||
// constraints for this service. But the orchestrator creates tasks
|
||||
// for all eligible nodes at the same time, so we should see all those
|
||||
// nodes represented among the up-to-date tasks.
|
||||
nodeCount := len(tasksByNode)
|
||||
|
||||
if !u.initialized {
|
||||
if nodeCount == 0 {
|
||||
// Two possibilities: either the orchestrator hasn't created
|
||||
// the tasks yet, or the service doesn't meet constraints for
|
||||
// any node. Either way, we wait.
|
||||
u.progressOut.WriteProgress(progress.Progress{
|
||||
ID: "overall progress",
|
||||
Action: "waiting for new tasks",
|
||||
})
|
||||
return false, nil
|
||||
}
|
||||
|
||||
writeOverallProgress(u.progressOut, 0, nodeCount, rollback)
|
||||
u.initialized = true
|
||||
}
|
||||
|
||||
// If we had reached a converged state, check if we are still converged.
|
||||
if u.done {
|
||||
for _, task := range tasksByNode {
|
||||
if task.Status.State != swarm.TaskStateRunning {
|
||||
u.done = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
running := 0
|
||||
|
||||
for _, task := range tasksByNode {
|
||||
if node, nodeActive := activeNodes[task.NodeID]; nodeActive {
|
||||
if !u.done && nodeCount <= maxProgressBars {
|
||||
u.progressOut.WriteProgress(progress.Progress{
|
||||
ID: stringid.TruncateID(node.ID),
|
||||
Action: fmt.Sprintf("%-[1]*s", longestState, task.Status.State),
|
||||
Current: stateToProgress(task.Status.State, rollback),
|
||||
Total: maxProgress,
|
||||
HideCounts: true,
|
||||
})
|
||||
}
|
||||
if task.Status.State == swarm.TaskStateRunning {
|
||||
running++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !u.done {
|
||||
writeOverallProgress(u.progressOut, running, nodeCount, rollback)
|
||||
|
||||
if running == nodeCount {
|
||||
u.done = true
|
||||
}
|
||||
}
|
||||
|
||||
return running == nodeCount, nil
|
||||
}
|
||||
127
cli/command/service/ps.go
Normal file
127
cli/command/service/ps.go
Normal file
@ -0,0 +1,127 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
swarmtypes "github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/docker/cli/command/formatter"
|
||||
"github.com/docker/docker/cli/command/idresolver"
|
||||
"github.com/docker/docker/cli/command/node"
|
||||
"github.com/docker/docker/cli/command/task"
|
||||
"github.com/docker/docker/opts"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type psOptions struct {
|
||||
services []string
|
||||
quiet bool
|
||||
noResolve bool
|
||||
noTrunc bool
|
||||
format string
|
||||
filter opts.FilterOpt
|
||||
}
|
||||
|
||||
func newPsCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
opts := psOptions{filter: opts.NewFilterOpt()}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "ps [OPTIONS] SERVICE [SERVICE...]",
|
||||
Short: "List the tasks of one or more services",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.services = args
|
||||
return runPS(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display task IDs")
|
||||
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
|
||||
flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names")
|
||||
flags.StringVar(&opts.format, "format", "", "Pretty-print tasks using a Go template")
|
||||
flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runPS(dockerCli *command.DockerCli, opts psOptions) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
filter := opts.filter.Value()
|
||||
|
||||
serviceIDFilter := filters.NewArgs()
|
||||
serviceNameFilter := filters.NewArgs()
|
||||
for _, service := range opts.services {
|
||||
// default to container runtime
|
||||
serviceIDFilter.Add("id", service)
|
||||
serviceIDFilter.Add("runtime", string(swarmtypes.RuntimeContainer))
|
||||
serviceNameFilter.Add("name", service)
|
||||
serviceNameFilter.Add("runtime", string(swarmtypes.RuntimeContainer))
|
||||
}
|
||||
serviceByIDList, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: serviceIDFilter})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serviceByNameList, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: serviceNameFilter})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, service := range opts.services {
|
||||
serviceCount := 0
|
||||
// Lookup by ID/Prefix
|
||||
for _, serviceEntry := range serviceByIDList {
|
||||
if strings.HasPrefix(serviceEntry.ID, service) {
|
||||
filter.Add("service", serviceEntry.ID)
|
||||
serviceCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup by Name/Prefix
|
||||
for _, serviceEntry := range serviceByNameList {
|
||||
if strings.HasPrefix(serviceEntry.Spec.Annotations.Name, service) {
|
||||
filter.Add("service", serviceEntry.ID)
|
||||
serviceCount++
|
||||
}
|
||||
}
|
||||
// If nothing has been found, return immediately.
|
||||
if serviceCount == 0 {
|
||||
return errors.Errorf("no such services: %s", service)
|
||||
}
|
||||
}
|
||||
|
||||
if filter.Include("node") {
|
||||
nodeFilters := filter.Get("node")
|
||||
for _, nodeFilter := range nodeFilters {
|
||||
nodeReference, err := node.Reference(ctx, client, nodeFilter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filter.Del("node", nodeFilter)
|
||||
filter.Add("node", nodeReference)
|
||||
}
|
||||
}
|
||||
|
||||
tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: filter})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
format := opts.format
|
||||
if len(format) == 0 {
|
||||
if len(dockerCli.ConfigFile().TasksFormat) > 0 && !opts.quiet {
|
||||
format = dockerCli.ConfigFile().TasksFormat
|
||||
} else {
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
}
|
||||
|
||||
return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), !opts.noTrunc, opts.quiet, format)
|
||||
}
|
||||
48
cli/command/service/remove.go
Normal file
48
cli/command/service/remove.go
Normal file
@ -0,0 +1,48 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "rm SERVICE [SERVICE...]",
|
||||
Aliases: []string{"remove"},
|
||||
Short: "Remove one or more services",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runRemove(dockerCli, args)
|
||||
},
|
||||
}
|
||||
cmd.Flags()
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runRemove(dockerCli *command.DockerCli, sids []string) error {
|
||||
client := dockerCli.Client()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
var errs []string
|
||||
for _, sid := range sids {
|
||||
err := client.ServiceRemove(ctx, sid)
|
||||
if err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(dockerCli.Out(), "%s\n", sid)
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return errors.Errorf(strings.Join(errs, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
97
cli/command/service/scale.go
Normal file
97
cli/command/service/scale.go
Normal file
@ -0,0 +1,97 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newScaleCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "scale SERVICE=REPLICAS [SERVICE=REPLICAS...]",
|
||||
Short: "Scale one or multiple replicated services",
|
||||
Args: scaleArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runScale(dockerCli, args)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func scaleArgs(cmd *cobra.Command, args []string) error {
|
||||
if err := cli.RequiresMinArgs(1)(cmd, args); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, arg := range args {
|
||||
if parts := strings.SplitN(arg, "=", 2); len(parts) != 2 {
|
||||
return errors.Errorf(
|
||||
"Invalid scale specifier '%s'.\nSee '%s --help'.\n\nUsage: %s\n\n%s",
|
||||
arg,
|
||||
cmd.CommandPath(),
|
||||
cmd.UseLine(),
|
||||
cmd.Short,
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runScale(dockerCli *command.DockerCli, args []string) error {
|
||||
var errs []string
|
||||
for _, arg := range args {
|
||||
parts := strings.SplitN(arg, "=", 2)
|
||||
serviceID, scaleStr := parts[0], parts[1]
|
||||
|
||||
// validate input arg scale number
|
||||
scale, err := strconv.ParseUint(scaleStr, 10, 64)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Sprintf("%s: invalid replicas value %s: %v", serviceID, scaleStr, err))
|
||||
continue
|
||||
}
|
||||
|
||||
if err := runServiceScale(dockerCli, serviceID, scale); err != nil {
|
||||
errs = append(errs, fmt.Sprintf("%s: %v", serviceID, err))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) == 0 {
|
||||
return nil
|
||||
}
|
||||
return errors.Errorf(strings.Join(errs, "\n"))
|
||||
}
|
||||
|
||||
func runServiceScale(dockerCli *command.DockerCli, serviceID string, scale uint64) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
service, _, err := client.ServiceInspectWithRaw(ctx, serviceID, types.ServiceInspectOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serviceMode := &service.Spec.Mode
|
||||
if serviceMode.Replicated == nil {
|
||||
return errors.Errorf("scale can only be used with replicated mode")
|
||||
}
|
||||
|
||||
serviceMode.Replicated.Replicas = &scale
|
||||
|
||||
response, err := client.ServiceUpdate(ctx, service.ID, service.Version, service.Spec, types.ServiceUpdateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, warning := range response.Warnings {
|
||||
fmt.Fprintln(dockerCli.Err(), warning)
|
||||
}
|
||||
|
||||
fmt.Fprintf(dockerCli.Out(), "%s scaled to %d\n", serviceID, scale)
|
||||
return nil
|
||||
}
|
||||
87
cli/command/service/trust.go
Normal file
87
cli/command/service/trust.go
Normal file
@ -0,0 +1,87 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/docker/cli/trust"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/notary/tuf/data"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func resolveServiceImageDigest(dockerCli *command.DockerCli, service *swarm.ServiceSpec) error {
|
||||
if !command.IsTrusted() {
|
||||
// Digests are resolved by the daemon when not using content
|
||||
// trust.
|
||||
return nil
|
||||
}
|
||||
|
||||
ref, err := reference.ParseAnyReference(service.TaskTemplate.ContainerSpec.Image)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "invalid reference %s", service.TaskTemplate.ContainerSpec.Image)
|
||||
}
|
||||
|
||||
// If reference does not have digest (is not canonical nor image id)
|
||||
if _, ok := ref.(reference.Digested); !ok {
|
||||
namedRef, ok := ref.(reference.Named)
|
||||
if !ok {
|
||||
return errors.New("failed to resolve image digest using content trust: reference is not named")
|
||||
}
|
||||
namedRef = reference.TagNameOnly(namedRef)
|
||||
taggedRef, ok := namedRef.(reference.NamedTagged)
|
||||
if !ok {
|
||||
return errors.New("failed to resolve image digest using content trust: reference is not tagged")
|
||||
}
|
||||
|
||||
resolvedImage, err := trustedResolveDigest(context.Background(), dockerCli, taggedRef)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to resolve image digest using content trust")
|
||||
}
|
||||
resolvedFamiliar := reference.FamiliarString(resolvedImage)
|
||||
logrus.Debugf("resolved image tag to %s using content trust", resolvedFamiliar)
|
||||
service.TaskTemplate.ContainerSpec.Image = resolvedFamiliar
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func trustedResolveDigest(ctx context.Context, cli *command.DockerCli, ref reference.NamedTagged) (reference.Canonical, error) {
|
||||
repoInfo, err := registry.ParseRepositoryInfo(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authConfig := command.ResolveAuthConfig(ctx, cli, repoInfo.Index)
|
||||
|
||||
notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error establishing connection to trust repository")
|
||||
}
|
||||
|
||||
t, err := notaryRepo.GetTargetByName(ref.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole)
|
||||
if err != nil {
|
||||
return nil, trust.NotaryError(repoInfo.Name.Name(), err)
|
||||
}
|
||||
// Only get the tag if it's in the top level targets role or the releases delegation role
|
||||
// ignore it if it's in any other delegation roles
|
||||
if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole {
|
||||
return nil, trust.NotaryError(repoInfo.Name.Name(), errors.Errorf("No trust data for %s", reference.FamiliarString(ref)))
|
||||
}
|
||||
|
||||
logrus.Debugf("retrieving target for %s role\n", t.Role)
|
||||
h, ok := t.Hashes["sha256"]
|
||||
if !ok {
|
||||
return nil, errors.New("no valid hash, expecting sha256")
|
||||
}
|
||||
|
||||
dgst := digest.NewDigestFromHex("sha256", hex.EncodeToString(h))
|
||||
|
||||
// Allow returning canonical reference with tag
|
||||
return reference.WithDigest(ref, dgst)
|
||||
}
|
||||
1018
cli/command/service/update.go
Normal file
1018
cli/command/service/update.go
Normal file
File diff suppressed because it is too large
Load Diff
496
cli/command/service/update_test.go
Normal file
496
cli/command/service/update_test.go
Normal file
@ -0,0 +1,496 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
mounttypes "github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestUpdateServiceArgs(t *testing.T) {
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
flags.Set("args", "the \"new args\"")
|
||||
|
||||
spec := &swarm.ServiceSpec{}
|
||||
cspec := &spec.TaskTemplate.ContainerSpec
|
||||
cspec.Args = []string{"old", "args"}
|
||||
|
||||
updateService(nil, nil, flags, spec)
|
||||
assert.Equal(t, []string{"the", "new args"}, cspec.Args)
|
||||
}
|
||||
|
||||
func TestUpdateLabels(t *testing.T) {
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
flags.Set("label-add", "toadd=newlabel")
|
||||
flags.Set("label-rm", "toremove")
|
||||
|
||||
labels := map[string]string{
|
||||
"toremove": "thelabeltoremove",
|
||||
"tokeep": "value",
|
||||
}
|
||||
|
||||
updateLabels(flags, &labels)
|
||||
assert.Len(t, labels, 2)
|
||||
assert.Equal(t, "value", labels["tokeep"])
|
||||
assert.Equal(t, "newlabel", labels["toadd"])
|
||||
}
|
||||
|
||||
func TestUpdateLabelsRemoveALabelThatDoesNotExist(t *testing.T) {
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
flags.Set("label-rm", "dne")
|
||||
|
||||
labels := map[string]string{"foo": "theoldlabel"}
|
||||
updateLabels(flags, &labels)
|
||||
assert.Len(t, labels, 1)
|
||||
}
|
||||
|
||||
func TestUpdatePlacementConstraints(t *testing.T) {
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
flags.Set("constraint-add", "node=toadd")
|
||||
flags.Set("constraint-rm", "node!=toremove")
|
||||
|
||||
placement := &swarm.Placement{
|
||||
Constraints: []string{"node!=toremove", "container=tokeep"},
|
||||
}
|
||||
|
||||
updatePlacementConstraints(flags, placement)
|
||||
require.Len(t, placement.Constraints, 2)
|
||||
assert.Equal(t, "container=tokeep", placement.Constraints[0])
|
||||
assert.Equal(t, "node=toadd", placement.Constraints[1])
|
||||
}
|
||||
|
||||
func TestUpdatePlacementPrefs(t *testing.T) {
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
flags.Set("placement-pref-add", "spread=node.labels.dc")
|
||||
flags.Set("placement-pref-rm", "spread=node.labels.rack")
|
||||
|
||||
placement := &swarm.Placement{
|
||||
Preferences: []swarm.PlacementPreference{
|
||||
{
|
||||
Spread: &swarm.SpreadOver{
|
||||
SpreadDescriptor: "node.labels.rack",
|
||||
},
|
||||
},
|
||||
{
|
||||
Spread: &swarm.SpreadOver{
|
||||
SpreadDescriptor: "node.labels.row",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
updatePlacementPreferences(flags, placement)
|
||||
require.Len(t, placement.Preferences, 2)
|
||||
assert.Equal(t, "node.labels.row", placement.Preferences[0].Spread.SpreadDescriptor)
|
||||
assert.Equal(t, "node.labels.dc", placement.Preferences[1].Spread.SpreadDescriptor)
|
||||
}
|
||||
|
||||
func TestUpdateEnvironment(t *testing.T) {
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
flags.Set("env-add", "toadd=newenv")
|
||||
flags.Set("env-rm", "toremove")
|
||||
|
||||
envs := []string{"toremove=theenvtoremove", "tokeep=value"}
|
||||
|
||||
updateEnvironment(flags, &envs)
|
||||
require.Len(t, envs, 2)
|
||||
// Order has been removed in updateEnvironment (map)
|
||||
sort.Strings(envs)
|
||||
assert.Equal(t, "toadd=newenv", envs[0])
|
||||
assert.Equal(t, "tokeep=value", envs[1])
|
||||
}
|
||||
|
||||
func TestUpdateEnvironmentWithDuplicateValues(t *testing.T) {
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
flags.Set("env-add", "foo=newenv")
|
||||
flags.Set("env-add", "foo=dupe")
|
||||
flags.Set("env-rm", "foo")
|
||||
|
||||
envs := []string{"foo=value"}
|
||||
|
||||
updateEnvironment(flags, &envs)
|
||||
assert.Len(t, envs, 0)
|
||||
}
|
||||
|
||||
func TestUpdateEnvironmentWithDuplicateKeys(t *testing.T) {
|
||||
// Test case for #25404
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
flags.Set("env-add", "A=b")
|
||||
|
||||
envs := []string{"A=c"}
|
||||
|
||||
updateEnvironment(flags, &envs)
|
||||
require.Len(t, envs, 1)
|
||||
assert.Equal(t, "A=b", envs[0])
|
||||
}
|
||||
|
||||
func TestUpdateGroups(t *testing.T) {
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
flags.Set("group-add", "wheel")
|
||||
flags.Set("group-add", "docker")
|
||||
flags.Set("group-rm", "root")
|
||||
flags.Set("group-add", "foo")
|
||||
flags.Set("group-rm", "docker")
|
||||
|
||||
groups := []string{"bar", "root"}
|
||||
|
||||
updateGroups(flags, &groups)
|
||||
require.Len(t, groups, 3)
|
||||
assert.Equal(t, "bar", groups[0])
|
||||
assert.Equal(t, "foo", groups[1])
|
||||
assert.Equal(t, "wheel", groups[2])
|
||||
}
|
||||
|
||||
func TestUpdateDNSConfig(t *testing.T) {
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
|
||||
// IPv4, with duplicates
|
||||
flags.Set("dns-add", "1.1.1.1")
|
||||
flags.Set("dns-add", "1.1.1.1")
|
||||
flags.Set("dns-add", "2.2.2.2")
|
||||
flags.Set("dns-rm", "3.3.3.3")
|
||||
flags.Set("dns-rm", "2.2.2.2")
|
||||
// IPv6
|
||||
flags.Set("dns-add", "2001:db8:abc8::1")
|
||||
// Invalid dns record
|
||||
assert.EqualError(t, flags.Set("dns-add", "x.y.z.w"), "x.y.z.w is not an ip address")
|
||||
|
||||
// domains with duplicates
|
||||
flags.Set("dns-search-add", "example.com")
|
||||
flags.Set("dns-search-add", "example.com")
|
||||
flags.Set("dns-search-add", "example.org")
|
||||
flags.Set("dns-search-rm", "example.org")
|
||||
// Invalid dns search domain
|
||||
assert.EqualError(t, flags.Set("dns-search-add", "example$com"), "example$com is not a valid domain")
|
||||
|
||||
flags.Set("dns-option-add", "ndots:9")
|
||||
flags.Set("dns-option-rm", "timeout:3")
|
||||
|
||||
config := &swarm.DNSConfig{
|
||||
Nameservers: []string{"3.3.3.3", "5.5.5.5"},
|
||||
Search: []string{"localdomain"},
|
||||
Options: []string{"timeout:3"},
|
||||
}
|
||||
|
||||
updateDNSConfig(flags, &config)
|
||||
|
||||
require.Len(t, config.Nameservers, 3)
|
||||
assert.Equal(t, "1.1.1.1", config.Nameservers[0])
|
||||
assert.Equal(t, "2001:db8:abc8::1", config.Nameservers[1])
|
||||
assert.Equal(t, "5.5.5.5", config.Nameservers[2])
|
||||
|
||||
require.Len(t, config.Search, 2)
|
||||
assert.Equal(t, "example.com", config.Search[0])
|
||||
assert.Equal(t, "localdomain", config.Search[1])
|
||||
|
||||
require.Len(t, config.Options, 1)
|
||||
assert.Equal(t, config.Options[0], "ndots:9")
|
||||
}
|
||||
|
||||
func TestUpdateMounts(t *testing.T) {
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
flags.Set("mount-add", "type=volume,source=vol2,target=/toadd")
|
||||
flags.Set("mount-rm", "/toremove")
|
||||
|
||||
mounts := []mounttypes.Mount{
|
||||
{Target: "/toremove", Source: "vol1", Type: mounttypes.TypeBind},
|
||||
{Target: "/tokeep", Source: "vol3", Type: mounttypes.TypeBind},
|
||||
}
|
||||
|
||||
updateMounts(flags, &mounts)
|
||||
require.Len(t, mounts, 2)
|
||||
assert.Equal(t, "/toadd", mounts[0].Target)
|
||||
assert.Equal(t, "/tokeep", mounts[1].Target)
|
||||
}
|
||||
|
||||
func TestUpdateMountsWithDuplicateMounts(t *testing.T) {
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
flags.Set("mount-add", "type=volume,source=vol4,target=/toadd")
|
||||
|
||||
mounts := []mounttypes.Mount{
|
||||
{Target: "/tokeep1", Source: "vol1", Type: mounttypes.TypeBind},
|
||||
{Target: "/toadd", Source: "vol2", Type: mounttypes.TypeBind},
|
||||
{Target: "/tokeep2", Source: "vol3", Type: mounttypes.TypeBind},
|
||||
}
|
||||
|
||||
updateMounts(flags, &mounts)
|
||||
require.Len(t, mounts, 3)
|
||||
assert.Equal(t, "/tokeep1", mounts[0].Target)
|
||||
assert.Equal(t, "/tokeep2", mounts[1].Target)
|
||||
assert.Equal(t, "/toadd", mounts[2].Target)
|
||||
}
|
||||
|
||||
func TestUpdatePorts(t *testing.T) {
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
flags.Set("publish-add", "1000:1000")
|
||||
flags.Set("publish-rm", "333/udp")
|
||||
|
||||
portConfigs := []swarm.PortConfig{
|
||||
{TargetPort: 333, Protocol: swarm.PortConfigProtocolUDP},
|
||||
{TargetPort: 555},
|
||||
}
|
||||
|
||||
err := updatePorts(flags, &portConfigs)
|
||||
assert.NoError(t, err)
|
||||
require.Len(t, portConfigs, 2)
|
||||
// Do a sort to have the order (might have changed by map)
|
||||
targetPorts := []int{int(portConfigs[0].TargetPort), int(portConfigs[1].TargetPort)}
|
||||
sort.Ints(targetPorts)
|
||||
assert.Equal(t, 555, targetPorts[0])
|
||||
assert.Equal(t, 1000, targetPorts[1])
|
||||
}
|
||||
|
||||
func TestUpdatePortsDuplicate(t *testing.T) {
|
||||
// Test case for #25375
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
flags.Set("publish-add", "80:80")
|
||||
|
||||
portConfigs := []swarm.PortConfig{
|
||||
{
|
||||
TargetPort: 80,
|
||||
PublishedPort: 80,
|
||||
Protocol: swarm.PortConfigProtocolTCP,
|
||||
PublishMode: swarm.PortConfigPublishModeIngress,
|
||||
},
|
||||
}
|
||||
|
||||
err := updatePorts(flags, &portConfigs)
|
||||
assert.NoError(t, err)
|
||||
require.Len(t, portConfigs, 1)
|
||||
assert.Equal(t, uint32(80), portConfigs[0].TargetPort)
|
||||
}
|
||||
|
||||
func TestUpdateHealthcheckTable(t *testing.T) {
|
||||
type test struct {
|
||||
flags [][2]string
|
||||
initial *container.HealthConfig
|
||||
expected *container.HealthConfig
|
||||
err string
|
||||
}
|
||||
testCases := []test{
|
||||
{
|
||||
flags: [][2]string{{"no-healthcheck", "true"}},
|
||||
initial: &container.HealthConfig{Test: []string{"CMD-SHELL", "cmd1"}, Retries: 10},
|
||||
expected: &container.HealthConfig{Test: []string{"NONE"}},
|
||||
},
|
||||
{
|
||||
flags: [][2]string{{"health-cmd", "cmd1"}},
|
||||
initial: &container.HealthConfig{Test: []string{"NONE"}},
|
||||
expected: &container.HealthConfig{Test: []string{"CMD-SHELL", "cmd1"}},
|
||||
},
|
||||
{
|
||||
flags: [][2]string{{"health-retries", "10"}},
|
||||
initial: &container.HealthConfig{Test: []string{"NONE"}},
|
||||
expected: &container.HealthConfig{Retries: 10},
|
||||
},
|
||||
{
|
||||
flags: [][2]string{{"health-retries", "10"}},
|
||||
initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}},
|
||||
expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Retries: 10},
|
||||
},
|
||||
{
|
||||
flags: [][2]string{{"health-interval", "1m"}},
|
||||
initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}},
|
||||
expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Interval: time.Minute},
|
||||
},
|
||||
{
|
||||
flags: [][2]string{{"health-cmd", ""}},
|
||||
initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Retries: 10},
|
||||
expected: &container.HealthConfig{Retries: 10},
|
||||
},
|
||||
{
|
||||
flags: [][2]string{{"health-retries", "0"}},
|
||||
initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Retries: 10},
|
||||
expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}},
|
||||
},
|
||||
{
|
||||
flags: [][2]string{{"health-start-period", "1m"}},
|
||||
initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}},
|
||||
expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, StartPeriod: time.Minute},
|
||||
},
|
||||
{
|
||||
flags: [][2]string{{"health-cmd", "cmd1"}, {"no-healthcheck", "true"}},
|
||||
err: "--no-healthcheck conflicts with --health-* options",
|
||||
},
|
||||
{
|
||||
flags: [][2]string{{"health-interval", "10m"}, {"no-healthcheck", "true"}},
|
||||
err: "--no-healthcheck conflicts with --health-* options",
|
||||
},
|
||||
{
|
||||
flags: [][2]string{{"health-timeout", "1m"}, {"no-healthcheck", "true"}},
|
||||
err: "--no-healthcheck conflicts with --health-* options",
|
||||
},
|
||||
}
|
||||
for i, c := range testCases {
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
for _, flag := range c.flags {
|
||||
flags.Set(flag[0], flag[1])
|
||||
}
|
||||
cspec := &swarm.ContainerSpec{
|
||||
Healthcheck: c.initial,
|
||||
}
|
||||
err := updateHealthcheck(flags, cspec)
|
||||
if c.err != "" {
|
||||
assert.EqualError(t, err, c.err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
if !reflect.DeepEqual(cspec.Healthcheck, c.expected) {
|
||||
t.Errorf("incorrect result for test %d, expected health config:\n\t%#v\ngot:\n\t%#v", i, c.expected, cspec.Healthcheck)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateHosts(t *testing.T) {
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
flags.Set("host-add", "example.net:2.2.2.2")
|
||||
flags.Set("host-add", "ipv6.net:2001:db8:abc8::1")
|
||||
// remove with ipv6 should work
|
||||
flags.Set("host-rm", "example.net:2001:db8:abc8::1")
|
||||
// just hostname should work as well
|
||||
flags.Set("host-rm", "example.net")
|
||||
// bad format error
|
||||
assert.EqualError(t, flags.Set("host-add", "$example.com$"), `bad format for add-host: "$example.com$"`)
|
||||
|
||||
hosts := []string{"1.2.3.4 example.com", "4.3.2.1 example.org", "2001:db8:abc8::1 example.net"}
|
||||
|
||||
updateHosts(flags, &hosts)
|
||||
require.Len(t, hosts, 3)
|
||||
assert.Equal(t, "1.2.3.4 example.com", hosts[0])
|
||||
assert.Equal(t, "2001:db8:abc8::1 ipv6.net", hosts[1])
|
||||
assert.Equal(t, "4.3.2.1 example.org", hosts[2])
|
||||
}
|
||||
|
||||
func TestUpdatePortsRmWithProtocol(t *testing.T) {
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
flags.Set("publish-add", "8081:81")
|
||||
flags.Set("publish-add", "8082:82")
|
||||
flags.Set("publish-rm", "80")
|
||||
flags.Set("publish-rm", "81/tcp")
|
||||
flags.Set("publish-rm", "82/udp")
|
||||
|
||||
portConfigs := []swarm.PortConfig{
|
||||
{
|
||||
TargetPort: 80,
|
||||
PublishedPort: 8080,
|
||||
Protocol: swarm.PortConfigProtocolTCP,
|
||||
PublishMode: swarm.PortConfigPublishModeIngress,
|
||||
},
|
||||
}
|
||||
|
||||
err := updatePorts(flags, &portConfigs)
|
||||
assert.NoError(t, err)
|
||||
require.Len(t, portConfigs, 2)
|
||||
assert.Equal(t, uint32(81), portConfigs[0].TargetPort)
|
||||
assert.Equal(t, uint32(82), portConfigs[1].TargetPort)
|
||||
}
|
||||
|
||||
type secretAPIClientMock struct {
|
||||
listResult []swarm.Secret
|
||||
}
|
||||
|
||||
func (s secretAPIClientMock) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) {
|
||||
return s.listResult, nil
|
||||
}
|
||||
func (s secretAPIClientMock) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error) {
|
||||
return types.SecretCreateResponse{}, nil
|
||||
}
|
||||
func (s secretAPIClientMock) SecretRemove(ctx context.Context, id string) error {
|
||||
return nil
|
||||
}
|
||||
func (s secretAPIClientMock) SecretInspectWithRaw(ctx context.Context, name string) (swarm.Secret, []byte, error) {
|
||||
return swarm.Secret{}, []byte{}, nil
|
||||
}
|
||||
func (s secretAPIClientMock) SecretUpdate(ctx context.Context, id string, version swarm.Version, secret swarm.SecretSpec) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestUpdateSecretUpdateInPlace tests the ability to update the "target" of an secret with "docker service update"
|
||||
// by combining "--secret-rm" and "--secret-add" for the same secret.
|
||||
func TestUpdateSecretUpdateInPlace(t *testing.T) {
|
||||
apiClient := secretAPIClientMock{
|
||||
listResult: []swarm.Secret{
|
||||
{
|
||||
ID: "tn9qiblgnuuut11eufquw5dev",
|
||||
Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "foo"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
flags.Set("secret-add", "source=foo,target=foo2")
|
||||
flags.Set("secret-rm", "foo")
|
||||
|
||||
secrets := []*swarm.SecretReference{
|
||||
{
|
||||
File: &swarm.SecretReferenceFileTarget{
|
||||
Name: "foo",
|
||||
UID: "0",
|
||||
GID: "0",
|
||||
Mode: 292,
|
||||
},
|
||||
SecretID: "tn9qiblgnuuut11eufquw5dev",
|
||||
SecretName: "foo",
|
||||
},
|
||||
}
|
||||
|
||||
updatedSecrets, err := getUpdatedSecrets(apiClient, flags, secrets)
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.Len(t, updatedSecrets, 1)
|
||||
assert.Equal(t, "tn9qiblgnuuut11eufquw5dev", updatedSecrets[0].SecretID)
|
||||
assert.Equal(t, "foo", updatedSecrets[0].SecretName)
|
||||
assert.Equal(t, "foo2", updatedSecrets[0].File.Name)
|
||||
}
|
||||
|
||||
func TestUpdateReadOnly(t *testing.T) {
|
||||
spec := &swarm.ServiceSpec{}
|
||||
cspec := &spec.TaskTemplate.ContainerSpec
|
||||
|
||||
// Update with --read-only=true, changed to true
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
flags.Set("read-only", "true")
|
||||
updateService(nil, nil, flags, spec)
|
||||
assert.True(t, cspec.ReadOnly)
|
||||
|
||||
// Update without --read-only, no change
|
||||
flags = newUpdateCommand(nil).Flags()
|
||||
updateService(nil, nil, flags, spec)
|
||||
assert.True(t, cspec.ReadOnly)
|
||||
|
||||
// Update with --read-only=false, changed to false
|
||||
flags = newUpdateCommand(nil).Flags()
|
||||
flags.Set("read-only", "false")
|
||||
updateService(nil, nil, flags, spec)
|
||||
assert.False(t, cspec.ReadOnly)
|
||||
}
|
||||
|
||||
func TestUpdateStopSignal(t *testing.T) {
|
||||
spec := &swarm.ServiceSpec{}
|
||||
cspec := &spec.TaskTemplate.ContainerSpec
|
||||
|
||||
// Update with --stop-signal=SIGUSR1
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
flags.Set("stop-signal", "SIGUSR1")
|
||||
updateService(nil, nil, flags, spec)
|
||||
assert.Equal(t, "SIGUSR1", cspec.StopSignal)
|
||||
|
||||
// Update without --stop-signal, no change
|
||||
flags = newUpdateCommand(nil).Flags()
|
||||
updateService(nil, nil, flags, spec)
|
||||
assert.Equal(t, "SIGUSR1", cspec.StopSignal)
|
||||
|
||||
// Update with --stop-signal=SIGWINCH
|
||||
flags = newUpdateCommand(nil).Flags()
|
||||
flags.Set("stop-signal", "SIGWINCH")
|
||||
updateService(nil, nil, flags, spec)
|
||||
assert.Equal(t, "SIGWINCH", cspec.StopSignal)
|
||||
}
|
||||
Reference in New Issue
Block a user