Browse code

Unify swarm init and update options

Add api side validation and defaults for init and
join requests.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>

Tonis Tiigi authored on 2016/06/22 06:27:04
Showing 9 changed files
... ...
@@ -13,16 +13,17 @@ import (
13 13
 )
14 14
 
15 15
 type initOptions struct {
16
+	swarmOptions
16 17
 	listenAddr      NodeAddrOption
17
-	autoAccept      AutoAcceptOption
18 18
 	forceNewCluster bool
19
-	secret          string
20 19
 }
21 20
 
22 21
 func newInitCommand(dockerCli *client.DockerCli) *cobra.Command {
23 22
 	opts := initOptions{
24 23
 		listenAddr: NewListenAddrOption(),
25
-		autoAccept: NewAutoAcceptOption(),
24
+		swarmOptions: swarmOptions{
25
+			autoAccept: NewAutoAcceptOption(),
26
+		},
26 27
 	}
27 28
 
28 29
 	cmd := &cobra.Command{
... ...
@@ -36,9 +37,8 @@ func newInitCommand(dockerCli *client.DockerCli) *cobra.Command {
36 36
 
37 37
 	flags := cmd.Flags()
38 38
 	flags.Var(&opts.listenAddr, "listen-addr", "Listen address")
39
-	flags.Var(&opts.autoAccept, flagAutoAccept, "Auto acceptance policy (worker, manager, or none)")
40
-	flags.StringVar(&opts.secret, flagSecret, "", "Set secret value needed to accept nodes into cluster")
41 39
 	flags.BoolVar(&opts.forceNewCluster, "force-new-cluster", false, "Force create a new cluster from current state.")
40
+	addSwarmFlags(flags, &opts.swarmOptions)
42 41
 	return cmd
43 42
 }
44 43
 
... ...
@@ -49,13 +49,9 @@ func runInit(dockerCli *client.DockerCli, flags *pflag.FlagSet, opts initOptions
49 49
 	req := swarm.InitRequest{
50 50
 		ListenAddr:      opts.listenAddr.String(),
51 51
 		ForceNewCluster: opts.forceNewCluster,
52
+		Spec:            opts.swarmOptions.ToSpec(),
52 53
 	}
53 54
 
54
-	if flags.Changed(flagSecret) {
55
-		req.Spec.AcceptancePolicy.Policies = opts.autoAccept.Policies(&opts.secret)
56
-	} else {
57
-		req.Spec.AcceptancePolicy.Policies = opts.autoAccept.Policies(nil)
58
-	}
59 55
 	nodeID, err := client.SwarmInit(ctx, req)
60 56
 	if err != nil {
61 57
 		return err
... ...
@@ -2,16 +2,16 @@ package swarm
2 2
 
3 3
 import (
4 4
 	"fmt"
5
-	"net"
6
-	"strconv"
7 5
 	"strings"
6
+	"time"
8 7
 
8
+	"github.com/docker/docker/opts"
9 9
 	"github.com/docker/engine-api/types/swarm"
10
+	"github.com/spf13/pflag"
10 11
 )
11 12
 
12 13
 const (
13
-	defaultListenAddr        = "0.0.0.0"
14
-	defaultListenPort uint16 = 2377
14
+	defaultListenAddr = "0.0.0.0:2377"
15 15
 	// WORKER constant for worker name
16 16
 	WORKER = "WORKER"
17 17
 	// MANAGER constant for manager name
... ...
@@ -32,10 +32,17 @@ var (
32 32
 	}
33 33
 )
34 34
 
35
+type swarmOptions struct {
36
+	autoAccept          AutoAcceptOption
37
+	secret              string
38
+	taskHistoryLimit    int64
39
+	dispatcherHeartbeat time.Duration
40
+	nodeCertExpiry      time.Duration
41
+}
42
+
35 43
 // NodeAddrOption is a pflag.Value for listen and remote addresses
36 44
 type NodeAddrOption struct {
37 45
 	addr string
38
-	port uint16
39 46
 }
40 47
 
41 48
 // String prints the representation of this flag
... ...
@@ -45,25 +52,11 @@ func (a *NodeAddrOption) String() string {
45 45
 
46 46
 // Set the value for this flag
47 47
 func (a *NodeAddrOption) Set(value string) error {
48
-	if !strings.Contains(value, ":") {
49
-		a.addr = value
50
-		return nil
51
-	}
52
-
53
-	host, port, err := net.SplitHostPort(value)
54
-	if err != nil {
55
-		return fmt.Errorf("Invalid url, %v", err)
56
-	}
57
-
58
-	portInt, err := strconv.ParseUint(port, 10, 16)
48
+	addr, err := opts.ParseTCPAddr(value, a.addr)
59 49
 	if err != nil {
60
-		return fmt.Errorf("invalid url, %v", err)
61
-	}
62
-	a.port = uint16(portInt)
63
-
64
-	if host != "" {
65
-		a.addr = host
50
+		return err
66 51
 	}
52
+	a.addr = addr
67 53
 	return nil
68 54
 }
69 55
 
... ...
@@ -74,17 +67,17 @@ func (a *NodeAddrOption) Type() string {
74 74
 
75 75
 // Value returns the value of this option as addr:port
76 76
 func (a *NodeAddrOption) Value() string {
77
-	return net.JoinHostPort(a.addr, strconv.Itoa(int(a.port)))
77
+	return strings.TrimPrefix(a.addr, "tcp://")
78 78
 }
79 79
 
80 80
 // NewNodeAddrOption returns a new node address option
81
-func NewNodeAddrOption(host string, port uint16) NodeAddrOption {
82
-	return NodeAddrOption{addr: host, port: port}
81
+func NewNodeAddrOption(addr string) NodeAddrOption {
82
+	return NodeAddrOption{addr}
83 83
 }
84 84
 
85 85
 // NewListenAddrOption returns a NodeAddrOption with default values
86 86
 func NewListenAddrOption() NodeAddrOption {
87
-	return NewNodeAddrOption(defaultListenAddr, defaultListenPort)
87
+	return NewNodeAddrOption(defaultListenAddr)
88 88
 }
89 89
 
90 90
 // AutoAcceptOption is a value type for auto-accept policy
... ...
@@ -148,3 +141,24 @@ func (o *AutoAcceptOption) Policies(secret *string) []swarm.Policy {
148 148
 func NewAutoAcceptOption() AutoAcceptOption {
149 149
 	return AutoAcceptOption{values: make(map[string]bool)}
150 150
 }
151
+
152
+func addSwarmFlags(flags *pflag.FlagSet, opts *swarmOptions) {
153
+	flags.Var(&opts.autoAccept, flagAutoAccept, "Auto acceptance policy (worker, manager or none)")
154
+	flags.StringVar(&opts.secret, flagSecret, "", "Set secret value needed to accept nodes into cluster")
155
+	flags.Int64Var(&opts.taskHistoryLimit, flagTaskHistoryLimit, 10, "Task history retention limit")
156
+	flags.DurationVar(&opts.dispatcherHeartbeat, flagDispatcherHeartbeat, time.Duration(5*time.Second), "Dispatcher heartbeat period")
157
+	flags.DurationVar(&opts.nodeCertExpiry, flagCertExpiry, time.Duration(90*24*time.Hour), "Validity period for node certificates")
158
+}
159
+
160
+func (opts *swarmOptions) ToSpec() swarm.Spec {
161
+	spec := swarm.Spec{}
162
+	if opts.secret != "" {
163
+		spec.AcceptancePolicy.Policies = opts.autoAccept.Policies(&opts.secret)
164
+	} else {
165
+		spec.AcceptancePolicy.Policies = opts.autoAccept.Policies(nil)
166
+	}
167
+	spec.Orchestration.TaskHistoryRetentionLimit = opts.taskHistoryLimit
168
+	spec.Dispatcher.HeartbeatPeriod = uint64(opts.dispatcherHeartbeat.Nanoseconds())
169
+	spec.CAConfig.NodeCertExpiry = opts.nodeCertExpiry
170
+	return spec
171
+}
... ...
@@ -8,31 +8,33 @@ import (
8 8
 )
9 9
 
10 10
 func TestNodeAddrOptionSetHostAndPort(t *testing.T) {
11
-	opt := NewNodeAddrOption("old", 123)
11
+	opt := NewNodeAddrOption("old:123")
12 12
 	addr := "newhost:5555"
13 13
 	assert.NilError(t, opt.Set(addr))
14
-	assert.Equal(t, opt.addr, "newhost")
15
-	assert.Equal(t, opt.port, uint16(5555))
16 14
 	assert.Equal(t, opt.Value(), addr)
17 15
 }
18 16
 
19 17
 func TestNodeAddrOptionSetHostOnly(t *testing.T) {
20 18
 	opt := NewListenAddrOption()
21 19
 	assert.NilError(t, opt.Set("newhost"))
22
-	assert.Equal(t, opt.addr, "newhost")
23
-	assert.Equal(t, opt.port, defaultListenPort)
20
+	assert.Equal(t, opt.Value(), "newhost:2377")
21
+}
22
+
23
+func TestNodeAddrOptionSetHostOnlyIPv6(t *testing.T) {
24
+	opt := NewListenAddrOption()
25
+	assert.NilError(t, opt.Set("::1"))
26
+	assert.Equal(t, opt.Value(), "[::1]:2377")
24 27
 }
25 28
 
26 29
 func TestNodeAddrOptionSetPortOnly(t *testing.T) {
27 30
 	opt := NewListenAddrOption()
28 31
 	assert.NilError(t, opt.Set(":4545"))
29
-	assert.Equal(t, opt.addr, defaultListenAddr)
30
-	assert.Equal(t, opt.port, uint16(4545))
32
+	assert.Equal(t, opt.Value(), "0.0.0.0:4545")
31 33
 }
32 34
 
33 35
 func TestNodeAddrOptionSetInvalidFormat(t *testing.T) {
34 36
 	opt := NewListenAddrOption()
35
-	assert.Error(t, opt.Set("http://localhost:4545"), "Invalid url")
37
+	assert.Error(t, opt.Set("http://localhost:4545"), "Invalid")
36 38
 }
37 39
 
38 40
 func TestAutoAcceptOptionSetWorker(t *testing.T) {
... ...
@@ -2,7 +2,6 @@ package swarm
2 2
 
3 3
 import (
4 4
 	"fmt"
5
-	"time"
6 5
 
7 6
 	"golang.org/x/net/context"
8 7
 
... ...
@@ -13,16 +12,8 @@ import (
13 13
 	"github.com/spf13/pflag"
14 14
 )
15 15
 
16
-type updateOptions struct {
17
-	autoAccept          AutoAcceptOption
18
-	secret              string
19
-	taskHistoryLimit    int64
20
-	dispatcherHeartbeat time.Duration
21
-	nodeCertExpiry      time.Duration
22
-}
23
-
24 16
 func newUpdateCommand(dockerCli *client.DockerCli) *cobra.Command {
25
-	opts := updateOptions{autoAccept: NewAutoAcceptOption()}
17
+	opts := swarmOptions{autoAccept: NewAutoAcceptOption()}
26 18
 
27 19
 	cmd := &cobra.Command{
28 20
 		Use:   "update",
... ...
@@ -33,16 +24,11 @@ func newUpdateCommand(dockerCli *client.DockerCli) *cobra.Command {
33 33
 		},
34 34
 	}
35 35
 
36
-	flags := cmd.Flags()
37
-	flags.Var(&opts.autoAccept, flagAutoAccept, "Auto acceptance policy (worker, manager or none)")
38
-	flags.StringVar(&opts.secret, flagSecret, "", "Set secret value needed to accept nodes into cluster")
39
-	flags.Int64Var(&opts.taskHistoryLimit, flagTaskHistoryLimit, 10, "Task history retention limit")
40
-	flags.DurationVar(&opts.dispatcherHeartbeat, flagDispatcherHeartbeat, time.Duration(5*time.Second), "Dispatcher heartbeat period")
41
-	flags.DurationVar(&opts.nodeCertExpiry, flagCertExpiry, time.Duration(90*24*time.Hour), "Validity period for node certificates")
36
+	addSwarmFlags(cmd.Flags(), &opts)
42 37
 	return cmd
43 38
 }
44 39
 
45
-func runUpdate(dockerCli *client.DockerCli, flags *pflag.FlagSet, opts updateOptions) error {
40
+func runUpdate(dockerCli *client.DockerCli, flags *pflag.FlagSet, opts swarmOptions) error {
46 41
 	client := dockerCli.Client()
47 42
 	ctx := context.Background()
48 43
 
... ...
@@ -13,10 +13,12 @@ import (
13 13
 	"google.golang.org/grpc"
14 14
 
15 15
 	"github.com/Sirupsen/logrus"
16
+	"github.com/docker/distribution/digest"
16 17
 	"github.com/docker/docker/daemon/cluster/convert"
17 18
 	executorpkg "github.com/docker/docker/daemon/cluster/executor"
18 19
 	"github.com/docker/docker/daemon/cluster/executor/container"
19 20
 	"github.com/docker/docker/errors"
21
+	"github.com/docker/docker/opts"
20 22
 	"github.com/docker/docker/pkg/ioutils"
21 23
 	"github.com/docker/docker/runconfig"
22 24
 	apitypes "github.com/docker/engine-api/types"
... ...
@@ -30,6 +32,7 @@ const swarmDirName = "swarm"
30 30
 const controlSocket = "control.sock"
31 31
 const swarmConnectTimeout = 20 * time.Second
32 32
 const stateFile = "docker-state.json"
33
+const defaultAddr = "0.0.0.0:2377"
33 34
 
34 35
 const (
35 36
 	initialReconnectDelay = 100 * time.Millisecond
... ...
@@ -51,6 +54,26 @@ var ErrPendingSwarmExists = fmt.Errorf("This node is processing an existing join
51 51
 // ErrSwarmJoinTimeoutReached is returned when cluster join could not complete before timeout was reached.
52 52
 var ErrSwarmJoinTimeoutReached = fmt.Errorf("Timeout was reached before node was joined. Attempt to join the cluster will continue in the background. Use \"docker info\" command to see the current Swarm status of your node.")
53 53
 
54
+// defaultSpec contains some sane defaults if cluster options are missing on init
55
+var defaultSpec = types.Spec{
56
+	Raft: types.RaftConfig{
57
+		SnapshotInterval:           10000,
58
+		KeepOldSnapshots:           0,
59
+		LogEntriesForSlowFollowers: 500,
60
+		HeartbeatTick:              1,
61
+		ElectionTick:               3,
62
+	},
63
+	CAConfig: types.CAConfig{
64
+		NodeCertExpiry: 90 * 24 * time.Hour,
65
+	},
66
+	Dispatcher: types.DispatcherConfig{
67
+		HeartbeatPeriod: uint64((5 * time.Second).Nanoseconds()),
68
+	},
69
+	Orchestration: types.OrchestrationConfig{
70
+		TaskHistoryRetentionLimit: 10,
71
+	},
72
+}
73
+
54 74
 type state struct {
55 75
 	ListenAddr string
56 76
 }
... ...
@@ -282,6 +305,12 @@ func (c *Cluster) Init(req types.InitRequest) (string, error) {
282 282
 		c.conn = nil
283 283
 		c.ready = false
284 284
 	}
285
+
286
+	if err := validateAndSanitizeInitRequest(&req); err != nil {
287
+		c.Unlock()
288
+		return "", err
289
+	}
290
+
285 291
 	// todo: check current state existing
286 292
 	n, ctx, err := c.startNewNode(req.ForceNewCluster, req.ListenAddr, "", "", "", false)
287 293
 	if err != nil {
... ...
@@ -292,7 +321,7 @@ func (c *Cluster) Init(req types.InitRequest) (string, error) {
292 292
 
293 293
 	select {
294 294
 	case <-n.Ready():
295
-		if err := initAcceptancePolicy(n, req.Spec.AcceptancePolicy); err != nil {
295
+		if err := initClusterSpec(n, req.Spec); err != nil {
296 296
 			return "", err
297 297
 		}
298 298
 		go c.reconnectOnFailure(ctx)
... ...
@@ -319,10 +348,11 @@ func (c *Cluster) Join(req types.JoinRequest) error {
319 319
 		c.Unlock()
320 320
 		return errSwarmExists(node)
321 321
 	}
322
-	// todo: check current state existing
323
-	if len(req.RemoteAddrs) == 0 {
324
-		return fmt.Errorf("at least 1 RemoteAddr is required to join")
322
+	if err := validateAndSanitizeJoinRequest(&req); err != nil {
323
+		c.Unlock()
324
+		return err
325 325
 	}
326
+	// todo: check current state existing
326 327
 	n, ctx, err := c.startNewNode(false, req.ListenAddr, req.RemoteAddrs[0], req.Secret, req.CACertHash, req.Manager)
327 328
 	if err != nil {
328 329
 		c.Unlock()
... ...
@@ -1030,6 +1060,76 @@ func (c *Cluster) managerStats() (current bool, reachable int, unreachable int,
1030 1030
 	return
1031 1031
 }
1032 1032
 
1033
+func validateAndSanitizeInitRequest(req *types.InitRequest) error {
1034
+	var err error
1035
+	req.ListenAddr, err = validateAddr(req.ListenAddr)
1036
+	if err != nil {
1037
+		return fmt.Errorf("invalid ListenAddr %q: %v", req.ListenAddr, err)
1038
+	}
1039
+
1040
+	spec := &req.Spec
1041
+	// provide sane defaults instead of erroring
1042
+	if spec.Name == "" {
1043
+		spec.Name = "default"
1044
+	}
1045
+	if spec.Raft.SnapshotInterval == 0 {
1046
+		spec.Raft.SnapshotInterval = defaultSpec.Raft.SnapshotInterval
1047
+	}
1048
+	if spec.Raft.LogEntriesForSlowFollowers == 0 {
1049
+		spec.Raft.LogEntriesForSlowFollowers = defaultSpec.Raft.LogEntriesForSlowFollowers
1050
+	}
1051
+	if spec.Raft.ElectionTick == 0 {
1052
+		spec.Raft.ElectionTick = defaultSpec.Raft.ElectionTick
1053
+	}
1054
+	if spec.Raft.HeartbeatTick == 0 {
1055
+		spec.Raft.HeartbeatTick = defaultSpec.Raft.HeartbeatTick
1056
+	}
1057
+	if spec.Dispatcher.HeartbeatPeriod == 0 {
1058
+		spec.Dispatcher.HeartbeatPeriod = defaultSpec.Dispatcher.HeartbeatPeriod
1059
+	}
1060
+	if spec.CAConfig.NodeCertExpiry == 0 {
1061
+		spec.CAConfig.NodeCertExpiry = defaultSpec.CAConfig.NodeCertExpiry
1062
+	}
1063
+	if spec.Orchestration.TaskHistoryRetentionLimit == 0 {
1064
+		spec.Orchestration.TaskHistoryRetentionLimit = defaultSpec.Orchestration.TaskHistoryRetentionLimit
1065
+	}
1066
+	return nil
1067
+}
1068
+
1069
+func validateAndSanitizeJoinRequest(req *types.JoinRequest) error {
1070
+	var err error
1071
+	req.ListenAddr, err = validateAddr(req.ListenAddr)
1072
+	if err != nil {
1073
+		return fmt.Errorf("invalid ListenAddr %q: %v", req.ListenAddr, err)
1074
+	}
1075
+	if len(req.RemoteAddrs) == 0 {
1076
+		return fmt.Errorf("at least 1 RemoteAddr is required to join")
1077
+	}
1078
+	for i := range req.RemoteAddrs {
1079
+		req.RemoteAddrs[i], err = validateAddr(req.RemoteAddrs[i])
1080
+		if err != nil {
1081
+			return fmt.Errorf("invalid remoteAddr %q: %v", req.RemoteAddrs[i], err)
1082
+		}
1083
+	}
1084
+	if req.CACertHash != "" {
1085
+		if _, err := digest.ParseDigest(req.CACertHash); err != nil {
1086
+			return fmt.Errorf("invalid CACertHash %q, %v", req.CACertHash, err)
1087
+		}
1088
+	}
1089
+	return nil
1090
+}
1091
+
1092
+func validateAddr(addr string) (string, error) {
1093
+	if addr == "" {
1094
+		return addr, fmt.Errorf("invalid empty address")
1095
+	}
1096
+	newaddr, err := opts.ParseTCPAddr(addr, defaultAddr)
1097
+	if err != nil {
1098
+		return addr, nil
1099
+	}
1100
+	return strings.TrimPrefix(newaddr, "tcp://"), nil
1101
+}
1102
+
1033 1103
 func errSwarmExists(node *swarmagent.Node) error {
1034 1104
 	if node.NodeMembership() != swarmapi.NodeMembershipAccepted {
1035 1105
 		return ErrPendingSwarmExists
... ...
@@ -1037,7 +1137,7 @@ func errSwarmExists(node *swarmagent.Node) error {
1037 1037
 	return ErrSwarmExists
1038 1038
 }
1039 1039
 
1040
-func initAcceptancePolicy(node *swarmagent.Node, acceptancePolicy types.AcceptancePolicy) error {
1040
+func initClusterSpec(node *swarmagent.Node, spec types.Spec) error {
1041 1041
 	ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
1042 1042
 	for conn := range node.ListenControlSocket(ctx) {
1043 1043
 		if ctx.Err() != nil {
... ...
@@ -1061,15 +1161,14 @@ func initAcceptancePolicy(node *swarmagent.Node, acceptancePolicy types.Acceptan
1061 1061
 				cluster = lcr.Clusters[0]
1062 1062
 				break
1063 1063
 			}
1064
-			spec := &cluster.Spec
1065
-
1066
-			if err := convert.SwarmSpecUpdateAcceptancePolicy(spec, acceptancePolicy, nil); err != nil {
1064
+			newspec, err := convert.SwarmSpecToGRPCandMerge(spec, &cluster.Spec)
1065
+			if err != nil {
1067 1066
 				return fmt.Errorf("error updating cluster settings: %v", err)
1068 1067
 			}
1069
-			_, err := client.UpdateCluster(ctx, &swarmapi.UpdateClusterRequest{
1068
+			_, err = client.UpdateCluster(ctx, &swarmapi.UpdateClusterRequest{
1070 1069
 				ClusterID:      cluster.ID,
1071 1070
 				ClusterVersion: &cluster.Meta.Version,
1072
-				Spec:           spec,
1071
+				Spec:           &newspec,
1073 1072
 			})
1074 1073
 			if err != nil {
1075 1074
 				return fmt.Errorf("error updating cluster settings: %v", err)
... ...
@@ -656,6 +656,24 @@ loop0:
656 656
 	}
657 657
 }
658 658
 
659
+func (s *DockerSwarmSuite) TestApiSwarmInvalidAddress(c *check.C) {
660
+	d := s.AddDaemon(c, false, false)
661
+	req := swarm.InitRequest{
662
+		ListenAddr: "",
663
+	}
664
+	status, _, err := d.SockRequest("POST", "/swarm/init", req)
665
+	c.Assert(err, checker.IsNil)
666
+	c.Assert(status, checker.Equals, http.StatusInternalServerError)
667
+
668
+	req2 := swarm.JoinRequest{
669
+		ListenAddr:  "0.0.0.0:2377",
670
+		RemoteAddrs: []string{""},
671
+	}
672
+	status, _, err = d.SockRequest("POST", "/swarm/join", req2)
673
+	c.Assert(err, checker.IsNil)
674
+	c.Assert(status, checker.Equals, http.StatusInternalServerError)
675
+}
676
+
659 677
 func simpleTestService(s *swarm.Service) {
660 678
 	var ureplicas uint64
661 679
 	ureplicas = 1
... ...
@@ -74,3 +74,63 @@ func (s *DockerSwarmSuite) TestSwarmUpdate(c *check.C) {
74 74
 	spec = getSpec()
75 75
 	c.Assert(spec.CAConfig.NodeCertExpiry, checker.Equals, 30*time.Hour)
76 76
 }
77
+
78
+func (s *DockerSwarmSuite) TestSwarmInit(c *check.C) {
79
+	d := s.AddDaemon(c, false, false)
80
+
81
+	getSpec := func() swarm.Spec {
82
+		out, err := d.Cmd("swarm", "inspect")
83
+		c.Assert(err, checker.IsNil)
84
+		var sw []swarm.Swarm
85
+		c.Assert(json.Unmarshal([]byte(out), &sw), checker.IsNil)
86
+		c.Assert(len(sw), checker.Equals, 1)
87
+		return sw[0].Spec
88
+	}
89
+
90
+	out, err := d.Cmd("swarm", "init", "--cert-expiry", "30h", "--dispatcher-heartbeat", "11s", "--auto-accept", "manager", "--auto-accept", "worker", "--secret", "foo")
91
+	c.Assert(err, checker.IsNil, check.Commentf("out: %v", out))
92
+
93
+	spec := getSpec()
94
+	c.Assert(spec.CAConfig.NodeCertExpiry, checker.Equals, 30*time.Hour)
95
+	c.Assert(spec.Dispatcher.HeartbeatPeriod, checker.Equals, uint64(11*time.Second))
96
+
97
+	c.Assert(spec.AcceptancePolicy.Policies, checker.HasLen, 2)
98
+
99
+	for _, p := range spec.AcceptancePolicy.Policies {
100
+		c.Assert(p.Autoaccept, checker.Equals, true)
101
+		c.Assert(p.Secret, checker.NotNil)
102
+		c.Assert(*p.Secret, checker.Not(checker.Equals), "")
103
+	}
104
+
105
+	c.Assert(d.Leave(true), checker.IsNil)
106
+
107
+	out, err = d.Cmd("swarm", "init", "--auto-accept", "none")
108
+	c.Assert(err, checker.IsNil, check.Commentf("out: %v", out))
109
+
110
+	spec = getSpec()
111
+	c.Assert(spec.CAConfig.NodeCertExpiry, checker.Equals, 90*24*time.Hour)
112
+	c.Assert(spec.Dispatcher.HeartbeatPeriod, checker.Equals, uint64(5*time.Second))
113
+
114
+	c.Assert(spec.AcceptancePolicy.Policies, checker.HasLen, 2)
115
+
116
+	for _, p := range spec.AcceptancePolicy.Policies {
117
+		c.Assert(p.Autoaccept, checker.Equals, false)
118
+		c.Assert(p.Secret, checker.IsNil)
119
+	}
120
+
121
+}
122
+
123
+func (s *DockerSwarmSuite) TestSwarmInitIPv6(c *check.C) {
124
+	testRequires(c, IPv6)
125
+	d1 := s.AddDaemon(c, false, false)
126
+	out, err := d1.Cmd("swarm", "init", "--listen-addr", "::1")
127
+	c.Assert(err, checker.IsNil, check.Commentf("out: %v", out))
128
+
129
+	d2 := s.AddDaemon(c, false, false)
130
+	out, err = d2.Cmd("swarm", "join", "::1")
131
+	c.Assert(err, checker.IsNil, check.Commentf("out: %v", out))
132
+
133
+	out, err = d2.Cmd("info")
134
+	c.Assert(err, checker.IsNil, check.Commentf("out: %v", out))
135
+	c.Assert(out, checker.Contains, "Swarm: active")
136
+}
... ...
@@ -70,7 +70,7 @@ func parseDockerDaemonHost(addr string) (string, error) {
70 70
 
71 71
 	switch addrParts[0] {
72 72
 	case "tcp":
73
-		return parseTCPAddr(addrParts[1], DefaultTCPHost)
73
+		return ParseTCPAddr(addrParts[1], DefaultTCPHost)
74 74
 	case "unix":
75 75
 		return parseSimpleProtoAddr("unix", addrParts[1], DefaultUnixSocket)
76 76
 	case "npipe":
... ...
@@ -97,12 +97,12 @@ func parseSimpleProtoAddr(proto, addr, defaultAddr string) (string, error) {
97 97
 	return fmt.Sprintf("%s://%s", proto, addr), nil
98 98
 }
99 99
 
100
-// parseTCPAddr parses and validates that the specified address is a valid TCP
100
+// ParseTCPAddr parses and validates that the specified address is a valid TCP
101 101
 // address. It returns a formatted TCP address, either using the address parsed
102 102
 // from tryAddr, or the contents of defaultAddr if tryAddr is a blank string.
103 103
 // tryAddr is expected to have already been Trim()'d
104 104
 // defaultAddr must be in the full `tcp://host:port` form
105
-func parseTCPAddr(tryAddr string, defaultAddr string) (string, error) {
105
+func ParseTCPAddr(tryAddr string, defaultAddr string) (string, error) {
106 106
 	if tryAddr == "" || tryAddr == "tcp://" {
107 107
 		return defaultAddr, nil
108 108
 	}
... ...
@@ -127,9 +127,12 @@ func parseTCPAddr(tryAddr string, defaultAddr string) (string, error) {
127 127
 	if err != nil {
128 128
 		return "", err
129 129
 	}
130
-
131 130
 	host, port, err := net.SplitHostPort(u.Host)
132 131
 	if err != nil {
132
+		// try port addition once
133
+		host, port, err = net.SplitHostPort(net.JoinHostPort(u.Host, defaultPort))
134
+	}
135
+	if err != nil {
133 136
 		return "", fmt.Errorf("Invalid bind address format: %s", tryAddr)
134 137
 	}
135 138
 
... ...
@@ -130,12 +130,12 @@ func TestParseTCP(t *testing.T) {
130 130
 		"localhost:5555/path":         "tcp://localhost:5555/path",
131 131
 	}
132 132
 	for invalidAddr, expectedError := range invalids {
133
-		if addr, err := parseTCPAddr(invalidAddr, defaultHTTPHost); err == nil || err.Error() != expectedError {
133
+		if addr, err := ParseTCPAddr(invalidAddr, defaultHTTPHost); err == nil || err.Error() != expectedError {
134 134
 			t.Errorf("tcp %v address expected error %v return, got %s and addr %v", invalidAddr, expectedError, err, addr)
135 135
 		}
136 136
 	}
137 137
 	for validAddr, expectedAddr := range valids {
138
-		if addr, err := parseTCPAddr(validAddr, defaultHTTPHost); err != nil || addr != expectedAddr {
138
+		if addr, err := ParseTCPAddr(validAddr, defaultHTTPHost); err != nil || addr != expectedAddr {
139 139
 			t.Errorf("%v -> expected %v, got %v and addr %v", validAddr, expectedAddr, err, addr)
140 140
 		}
141 141
 	}