Browse code

Add support for swarm mode templating

Wire templating support of swarmkit for the engine, in order to be used
through services.

Signed-off-by: Vincent Demeester <vincent@sbr.pm>

Vincent Demeester authored on 2016/11/10 07:28:06
Showing 6 changed files
... ...
@@ -20,6 +20,7 @@ import (
20 20
 	"github.com/docker/swarmkit/agent/exec"
21 21
 	"github.com/docker/swarmkit/api"
22 22
 	"github.com/docker/swarmkit/protobuf/ptypes"
23
+	"github.com/docker/swarmkit/template"
23 24
 )
24 25
 
25 26
 const (
... ...
@@ -68,6 +69,17 @@ func (c *containerConfig) setTask(t *api.Task) error {
68 68
 	}
69 69
 
70 70
 	c.task = t
71
+
72
+	if t.Spec.GetContainer() != nil {
73
+		preparedSpec, err := template.ExpandContainerSpec(t)
74
+		if err != nil {
75
+			return err
76
+		}
77
+		c.task.Spec.Runtime = &api.TaskSpec_Container{
78
+			Container: preparedSpec,
79
+		}
80
+	}
81
+
71 82
 	return nil
72 83
 }
73 84
 
... ...
@@ -118,6 +118,21 @@ func (s *DockerSwarmSuite) TestSwarmNodeListHostname(c *check.C) {
118 118
 	c.Assert(strings.Split(out, "\n")[0], checker.Contains, "HOSTNAME")
119 119
 }
120 120
 
121
+func (s *DockerSwarmSuite) TestSwarmServiceTemplatingHostname(c *check.C) {
122
+	d := s.AddDaemon(c, true, true)
123
+
124
+	out, err := d.Cmd("service", "create", "--name", "test", "--hostname", "{{.Service.Name}}-{{.Task.Slot}}", "busybox", "top")
125
+	c.Assert(err, checker.IsNil, check.Commentf(out))
126
+
127
+	// make sure task has been deployed.
128
+	waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, 1)
129
+
130
+	containers := d.activeContainers()
131
+	out, err = d.Cmd("inspect", "--type", "container", "--format", "{{.Config.Hostname}}", containers[0])
132
+	c.Assert(err, checker.IsNil, check.Commentf(out))
133
+	c.Assert(strings.Split(out, "\n")[0], checker.Equals, "test-1", check.Commentf("hostname with templating invalid"))
134
+}
135
+
121 136
 // Test case for #24270
122 137
 func (s *DockerSwarmSuite) TestSwarmServiceListFilter(c *check.C) {
123 138
 	d := s.AddDaemon(c, true, true)
... ...
@@ -343,17 +358,17 @@ func (s *DockerSwarmSuite) TestSwarmContainerAutoStart(c *check.C) {
343 343
 	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
344 344
 
345 345
 	out, err = d.Cmd("run", "-id", "--restart=always", "--net=foo", "--name=test", "busybox", "top")
346
-	c.Assert(err, checker.IsNil)
346
+	c.Assert(err, checker.IsNil, check.Commentf(out))
347 347
 	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
348 348
 
349 349
 	out, err = d.Cmd("ps", "-q")
350
-	c.Assert(err, checker.IsNil)
350
+	c.Assert(err, checker.IsNil, check.Commentf(out))
351 351
 	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
352 352
 
353 353
 	d.Restart()
354 354
 
355 355
 	out, err = d.Cmd("ps", "-q")
356
-	c.Assert(err, checker.IsNil)
356
+	c.Assert(err, checker.IsNil, check.Commentf(out))
357 357
 	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
358 358
 }
359 359
 
... ...
@@ -361,20 +376,20 @@ func (s *DockerSwarmSuite) TestSwarmContainerEndpointOptions(c *check.C) {
361 361
 	d := s.AddDaemon(c, true, true)
362 362
 
363 363
 	out, err := d.Cmd("network", "create", "--attachable", "-d", "overlay", "foo")
364
-	c.Assert(err, checker.IsNil)
364
+	c.Assert(err, checker.IsNil, check.Commentf(out))
365 365
 	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
366 366
 
367 367
 	_, err = d.Cmd("run", "-d", "--net=foo", "--name=first", "--net-alias=first-alias", "busybox", "top")
368
-	c.Assert(err, checker.IsNil)
368
+	c.Assert(err, checker.IsNil, check.Commentf(out))
369 369
 
370 370
 	_, err = d.Cmd("run", "-d", "--net=foo", "--name=second", "busybox", "top")
371
-	c.Assert(err, checker.IsNil)
371
+	c.Assert(err, checker.IsNil, check.Commentf(out))
372 372
 
373 373
 	// ping first container and its alias
374 374
 	_, err = d.Cmd("exec", "second", "ping", "-c", "1", "first")
375
-	c.Assert(err, check.IsNil)
375
+	c.Assert(err, check.IsNil, check.Commentf(out))
376 376
 	_, err = d.Cmd("exec", "second", "ping", "-c", "1", "first-alias")
377
-	c.Assert(err, check.IsNil)
377
+	c.Assert(err, check.IsNil, check.Commentf(out))
378 378
 }
379 379
 
380 380
 func (s *DockerSwarmSuite) TestSwarmContainerAttachByNetworkId(c *check.C) {
... ...
@@ -14,6 +14,7 @@ import (
14 14
 	"github.com/docker/swarmkit/manager/constraint"
15 15
 	"github.com/docker/swarmkit/manager/state/store"
16 16
 	"github.com/docker/swarmkit/protobuf/ptypes"
17
+	"github.com/docker/swarmkit/template"
17 18
 	"golang.org/x/net/context"
18 19
 	"google.golang.org/grpc"
19 20
 	"google.golang.org/grpc/codes"
... ...
@@ -168,7 +169,28 @@ func validateTask(taskSpec api.TaskSpec) error {
168 168
 		return grpc.Errorf(codes.Unimplemented, "RuntimeSpec: unimplemented runtime in service spec")
169 169
 	}
170 170
 
171
-	if err := validateContainerSpec(taskSpec.GetContainer()); err != nil {
171
+	// Building a empty/dummy Task to validate the templating and
172
+	// the resulting container spec as well. This is a *best effort*
173
+	// validation.
174
+	preparedSpec, err := template.ExpandContainerSpec(&api.Task{
175
+		Spec:      taskSpec,
176
+		ServiceID: "serviceid",
177
+		Slot:      1,
178
+		NodeID:    "nodeid",
179
+		Networks:  []*api.NetworkAttachment{},
180
+		Annotations: api.Annotations{
181
+			Name: "taskname",
182
+		},
183
+		ServiceAnnotations: api.Annotations{
184
+			Name: "servicename",
185
+		},
186
+		Endpoint:  &api.Endpoint{},
187
+		LogDriver: taskSpec.LogDriver,
188
+	})
189
+	if err != nil {
190
+		return grpc.Errorf(codes.InvalidArgument, err.Error())
191
+	}
192
+	if err := validateContainerSpec(preparedSpec); err != nil {
172 193
 		return err
173 194
 	}
174 195
 
175 196
new file mode 100644
... ...
@@ -0,0 +1,72 @@
0
+package template
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+
6
+	"github.com/docker/swarmkit/api"
7
+	"github.com/docker/swarmkit/api/naming"
8
+)
9
+
10
+// Context defines the strict set of values that can be injected into a
11
+// template expression in SwarmKit data structure.
12
+type Context struct {
13
+	Service struct {
14
+		ID     string
15
+		Name   string
16
+		Labels map[string]string
17
+	}
18
+
19
+	Node struct {
20
+		ID string
21
+	}
22
+
23
+	Task struct {
24
+		ID   string
25
+		Name string
26
+		Slot string
27
+
28
+		// NOTE(stevvooe): Why no labels here? Tasks don't actually have labels
29
+		// (from a user perspective). The labels are part of the container! If
30
+		// one wants to use labels for templating, use service labels!
31
+	}
32
+}
33
+
34
+// NewContextFromTask returns a new template context from the data available in
35
+// task. The provided context can then be used to populate runtime values in a
36
+// ContainerSpec.
37
+func NewContextFromTask(t *api.Task) (ctx Context) {
38
+	ctx.Service.ID = t.ServiceID
39
+	ctx.Service.Name = t.ServiceAnnotations.Name
40
+	ctx.Service.Labels = t.ServiceAnnotations.Labels
41
+
42
+	ctx.Node.ID = t.NodeID
43
+
44
+	ctx.Task.ID = t.ID
45
+	ctx.Task.Name = naming.Task(t)
46
+
47
+	if t.Slot != 0 {
48
+		ctx.Task.Slot = fmt.Sprint(t.Slot)
49
+	} else {
50
+		// fall back to node id for slot when there is no slot
51
+		ctx.Task.Slot = t.NodeID
52
+	}
53
+
54
+	return
55
+}
56
+
57
+// Expand treats the string s as a template and populates it with values from
58
+// the context.
59
+func (ctx *Context) Expand(s string) (string, error) {
60
+	tmpl, err := newTemplate(s)
61
+	if err != nil {
62
+		return s, err
63
+	}
64
+
65
+	var buf bytes.Buffer
66
+	if err := tmpl.Execute(&buf, ctx); err != nil {
67
+		return s, err
68
+	}
69
+
70
+	return buf.String(), nil
71
+}
0 72
new file mode 100644
... ...
@@ -0,0 +1,118 @@
0
+package template
1
+
2
+import (
3
+	"fmt"
4
+	"strings"
5
+
6
+	"github.com/docker/swarmkit/api"
7
+	"github.com/pkg/errors"
8
+)
9
+
10
+// ExpandContainerSpec expands templated fields in the runtime using the task
11
+// state. Templating is all evaluated on the agent-side, before execution.
12
+//
13
+// Note that these are projected only on runtime values, since active task
14
+// values are typically manipulated in the manager.
15
+func ExpandContainerSpec(t *api.Task) (*api.ContainerSpec, error) {
16
+	container := t.Spec.GetContainer()
17
+	if container == nil {
18
+		return nil, errors.Errorf("task missing ContainerSpec to expand")
19
+	}
20
+
21
+	container = container.Copy()
22
+	ctx := NewContextFromTask(t)
23
+
24
+	var err error
25
+	container.Env, err = expandEnv(ctx, container.Env)
26
+	if err != nil {
27
+		return container, errors.Wrap(err, "expanding env failed")
28
+	}
29
+
30
+	// For now, we only allow templating of string-based mount fields
31
+	container.Mounts, err = expandMounts(ctx, container.Mounts)
32
+	if err != nil {
33
+		return container, errors.Wrap(err, "expanding mounts failed")
34
+	}
35
+
36
+	container.Hostname, err = ctx.Expand(container.Hostname)
37
+	return container, errors.Wrap(err, "expanding hostname failed")
38
+}
39
+
40
+func expandMounts(ctx Context, mounts []api.Mount) ([]api.Mount, error) {
41
+	if len(mounts) == 0 {
42
+		return mounts, nil
43
+	}
44
+
45
+	expanded := make([]api.Mount, len(mounts))
46
+	for i, mount := range mounts {
47
+		var err error
48
+		mount.Source, err = ctx.Expand(mount.Source)
49
+		if err != nil {
50
+			return mounts, errors.Wrapf(err, "expanding mount source %q", mount.Source)
51
+		}
52
+
53
+		mount.Target, err = ctx.Expand(mount.Target)
54
+		if err != nil {
55
+			return mounts, errors.Wrapf(err, "expanding mount target %q", mount.Target)
56
+		}
57
+
58
+		if mount.VolumeOptions != nil {
59
+			mount.VolumeOptions.Labels, err = expandMap(ctx, mount.VolumeOptions.Labels)
60
+			if err != nil {
61
+				return mounts, errors.Wrap(err, "expanding volume labels")
62
+			}
63
+
64
+			if mount.VolumeOptions.DriverConfig != nil {
65
+				mount.VolumeOptions.DriverConfig.Options, err = expandMap(ctx, mount.VolumeOptions.DriverConfig.Options)
66
+				if err != nil {
67
+					return mounts, errors.Wrap(err, "expanding volume driver config")
68
+				}
69
+			}
70
+		}
71
+
72
+		expanded[i] = mount
73
+	}
74
+
75
+	return expanded, nil
76
+}
77
+
78
+func expandMap(ctx Context, m map[string]string) (map[string]string, error) {
79
+	var (
80
+		n   = make(map[string]string, len(m))
81
+		err error
82
+	)
83
+
84
+	for k, v := range m {
85
+		v, err = ctx.Expand(v)
86
+		if err != nil {
87
+			return m, errors.Wrapf(err, "expanding map entry %q=%q", k, v)
88
+		}
89
+
90
+		n[k] = v
91
+	}
92
+
93
+	return n, nil
94
+}
95
+
96
+func expandEnv(ctx Context, values []string) ([]string, error) {
97
+	var result []string
98
+	for _, value := range values {
99
+		var (
100
+			parts = strings.SplitN(value, "=", 2)
101
+			entry = parts[0]
102
+		)
103
+
104
+		if len(parts) > 1 {
105
+			expanded, err := ctx.Expand(parts[1])
106
+			if err != nil {
107
+				return values, errors.Wrapf(err, "expanding env %q", value)
108
+			}
109
+
110
+			entry = fmt.Sprintf("%s=%s", entry, expanded)
111
+		}
112
+
113
+		result = append(result, entry)
114
+	}
115
+
116
+	return result, nil
117
+}
0 118
new file mode 100644
... ...
@@ -0,0 +1,18 @@
0
+package template
1
+
2
+import (
3
+	"strings"
4
+	"text/template"
5
+)
6
+
7
+// funcMap defines functions for our template system.
8
+var funcMap = template.FuncMap{
9
+	"join": func(s ...string) string {
10
+		// first arg is sep, remaining args are strings to join
11
+		return strings.Join(s[1:], s[0])
12
+	},
13
+}
14
+
15
+func newTemplate(s string) (*template.Template, error) {
16
+	return template.New("expansion").Option("missingkey=error").Funcs(funcMap).Parse(s)
17
+}