Import docker/docker/cli

Signed-off-by: Daniel Nephin <dnephin@gmail.com>
This commit is contained in:
Daniel Nephin
2017-04-17 17:40:59 -04:00
325 changed files with 38711 additions and 0 deletions

View 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
}

View 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)
}

View 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
}

View 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
}

View 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
View 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
View 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
View 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"
)

View 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")
}

View 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
}

View 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
View 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)
}

View 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
}

View 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
}

View 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)
}

File diff suppressed because it is too large Load Diff

View 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)
}