Wire templating support of swarmkit for the engine, in order to be used
through services.
Signed-off-by: Vincent Demeester <vincent@sbr.pm>
| ... | ... |
@@ -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 |
+} |