diff --git a/daemon/cluster/executor/container/container.go b/daemon/cluster/executor/container/container.go index 5598c6b1d4..ab24d072a8 100644 --- a/daemon/cluster/executor/container/container.go +++ b/daemon/cluster/executor/container/container.go @@ -20,6 +20,7 @@ import ( "github.com/docker/swarmkit/agent/exec" "github.com/docker/swarmkit/api" "github.com/docker/swarmkit/protobuf/ptypes" + "github.com/docker/swarmkit/template" ) const ( @@ -68,6 +69,17 @@ func (c *containerConfig) setTask(t *api.Task) error { } c.task = t + + if t.Spec.GetContainer() != nil { + preparedSpec, err := template.ExpandContainerSpec(t) + if err != nil { + return err + } + c.task.Spec.Runtime = &api.TaskSpec_Container{ + Container: preparedSpec, + } + } + return nil } diff --git a/integration-cli/docker_cli_swarm_test.go b/integration-cli/docker_cli_swarm_test.go index bee60fd29f..65982f97cf 100644 --- a/integration-cli/docker_cli_swarm_test.go +++ b/integration-cli/docker_cli_swarm_test.go @@ -118,6 +118,21 @@ func (s *DockerSwarmSuite) TestSwarmNodeListHostname(c *check.C) { c.Assert(strings.Split(out, "\n")[0], checker.Contains, "HOSTNAME") } +func (s *DockerSwarmSuite) TestSwarmServiceTemplatingHostname(c *check.C) { + d := s.AddDaemon(c, true, true) + + out, err := d.Cmd("service", "create", "--name", "test", "--hostname", "{{.Service.Name}}-{{.Task.Slot}}", "busybox", "top") + c.Assert(err, checker.IsNil, check.Commentf(out)) + + // make sure task has been deployed. + waitAndAssert(c, defaultReconciliationTimeout, d.checkActiveContainerCount, checker.Equals, 1) + + containers := d.activeContainers() + out, err = d.Cmd("inspect", "--type", "container", "--format", "{{.Config.Hostname}}", containers[0]) + c.Assert(err, checker.IsNil, check.Commentf(out)) + c.Assert(strings.Split(out, "\n")[0], checker.Equals, "test-1", check.Commentf("hostname with templating invalid")) +} + // Test case for #24270 func (s *DockerSwarmSuite) TestSwarmServiceListFilter(c *check.C) { d := s.AddDaemon(c, true, true) @@ -343,17 +358,17 @@ func (s *DockerSwarmSuite) TestSwarmContainerAutoStart(c *check.C) { c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "") out, err = d.Cmd("run", "-id", "--restart=always", "--net=foo", "--name=test", "busybox", "top") - c.Assert(err, checker.IsNil) + c.Assert(err, checker.IsNil, check.Commentf(out)) c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "") out, err = d.Cmd("ps", "-q") - c.Assert(err, checker.IsNil) + c.Assert(err, checker.IsNil, check.Commentf(out)) c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "") d.Restart() out, err = d.Cmd("ps", "-q") - c.Assert(err, checker.IsNil) + c.Assert(err, checker.IsNil, check.Commentf(out)) c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "") } @@ -361,20 +376,20 @@ func (s *DockerSwarmSuite) TestSwarmContainerEndpointOptions(c *check.C) { d := s.AddDaemon(c, true, true) out, err := d.Cmd("network", "create", "--attachable", "-d", "overlay", "foo") - c.Assert(err, checker.IsNil) + c.Assert(err, checker.IsNil, check.Commentf(out)) c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "") _, err = d.Cmd("run", "-d", "--net=foo", "--name=first", "--net-alias=first-alias", "busybox", "top") - c.Assert(err, checker.IsNil) + c.Assert(err, checker.IsNil, check.Commentf(out)) _, err = d.Cmd("run", "-d", "--net=foo", "--name=second", "busybox", "top") - c.Assert(err, checker.IsNil) + c.Assert(err, checker.IsNil, check.Commentf(out)) // ping first container and its alias _, err = d.Cmd("exec", "second", "ping", "-c", "1", "first") - c.Assert(err, check.IsNil) + c.Assert(err, check.IsNil, check.Commentf(out)) _, err = d.Cmd("exec", "second", "ping", "-c", "1", "first-alias") - c.Assert(err, check.IsNil) + c.Assert(err, check.IsNil, check.Commentf(out)) } func (s *DockerSwarmSuite) TestSwarmContainerAttachByNetworkId(c *check.C) { diff --git a/vendor/github.com/docker/swarmkit/manager/controlapi/service.go b/vendor/github.com/docker/swarmkit/manager/controlapi/service.go index df0cff08e6..915574a92d 100644 --- a/vendor/github.com/docker/swarmkit/manager/controlapi/service.go +++ b/vendor/github.com/docker/swarmkit/manager/controlapi/service.go @@ -14,6 +14,7 @@ import ( "github.com/docker/swarmkit/manager/constraint" "github.com/docker/swarmkit/manager/state/store" "github.com/docker/swarmkit/protobuf/ptypes" + "github.com/docker/swarmkit/template" "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -168,7 +169,28 @@ func validateTask(taskSpec api.TaskSpec) error { return grpc.Errorf(codes.Unimplemented, "RuntimeSpec: unimplemented runtime in service spec") } - if err := validateContainerSpec(taskSpec.GetContainer()); err != nil { + // Building a empty/dummy Task to validate the templating and + // the resulting container spec as well. This is a *best effort* + // validation. + preparedSpec, err := template.ExpandContainerSpec(&api.Task{ + Spec: taskSpec, + ServiceID: "serviceid", + Slot: 1, + NodeID: "nodeid", + Networks: []*api.NetworkAttachment{}, + Annotations: api.Annotations{ + Name: "taskname", + }, + ServiceAnnotations: api.Annotations{ + Name: "servicename", + }, + Endpoint: &api.Endpoint{}, + LogDriver: taskSpec.LogDriver, + }) + if err != nil { + return grpc.Errorf(codes.InvalidArgument, err.Error()) + } + if err := validateContainerSpec(preparedSpec); err != nil { return err } diff --git a/vendor/github.com/docker/swarmkit/template/context.go b/vendor/github.com/docker/swarmkit/template/context.go new file mode 100644 index 0000000000..e3c3aab113 --- /dev/null +++ b/vendor/github.com/docker/swarmkit/template/context.go @@ -0,0 +1,72 @@ +package template + +import ( + "bytes" + "fmt" + + "github.com/docker/swarmkit/api" + "github.com/docker/swarmkit/api/naming" +) + +// Context defines the strict set of values that can be injected into a +// template expression in SwarmKit data structure. +type Context struct { + Service struct { + ID string + Name string + Labels map[string]string + } + + Node struct { + ID string + } + + Task struct { + ID string + Name string + Slot string + + // NOTE(stevvooe): Why no labels here? Tasks don't actually have labels + // (from a user perspective). The labels are part of the container! If + // one wants to use labels for templating, use service labels! + } +} + +// NewContextFromTask returns a new template context from the data available in +// task. The provided context can then be used to populate runtime values in a +// ContainerSpec. +func NewContextFromTask(t *api.Task) (ctx Context) { + ctx.Service.ID = t.ServiceID + ctx.Service.Name = t.ServiceAnnotations.Name + ctx.Service.Labels = t.ServiceAnnotations.Labels + + ctx.Node.ID = t.NodeID + + ctx.Task.ID = t.ID + ctx.Task.Name = naming.Task(t) + + if t.Slot != 0 { + ctx.Task.Slot = fmt.Sprint(t.Slot) + } else { + // fall back to node id for slot when there is no slot + ctx.Task.Slot = t.NodeID + } + + return +} + +// Expand treats the string s as a template and populates it with values from +// the context. +func (ctx *Context) Expand(s string) (string, error) { + tmpl, err := newTemplate(s) + if err != nil { + return s, err + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, ctx); err != nil { + return s, err + } + + return buf.String(), nil +} diff --git a/vendor/github.com/docker/swarmkit/template/expand.go b/vendor/github.com/docker/swarmkit/template/expand.go new file mode 100644 index 0000000000..75fbc09aee --- /dev/null +++ b/vendor/github.com/docker/swarmkit/template/expand.go @@ -0,0 +1,118 @@ +package template + +import ( + "fmt" + "strings" + + "github.com/docker/swarmkit/api" + "github.com/pkg/errors" +) + +// ExpandContainerSpec expands templated fields in the runtime using the task +// state. Templating is all evaluated on the agent-side, before execution. +// +// Note that these are projected only on runtime values, since active task +// values are typically manipulated in the manager. +func ExpandContainerSpec(t *api.Task) (*api.ContainerSpec, error) { + container := t.Spec.GetContainer() + if container == nil { + return nil, errors.Errorf("task missing ContainerSpec to expand") + } + + container = container.Copy() + ctx := NewContextFromTask(t) + + var err error + container.Env, err = expandEnv(ctx, container.Env) + if err != nil { + return container, errors.Wrap(err, "expanding env failed") + } + + // For now, we only allow templating of string-based mount fields + container.Mounts, err = expandMounts(ctx, container.Mounts) + if err != nil { + return container, errors.Wrap(err, "expanding mounts failed") + } + + container.Hostname, err = ctx.Expand(container.Hostname) + return container, errors.Wrap(err, "expanding hostname failed") +} + +func expandMounts(ctx Context, mounts []api.Mount) ([]api.Mount, error) { + if len(mounts) == 0 { + return mounts, nil + } + + expanded := make([]api.Mount, len(mounts)) + for i, mount := range mounts { + var err error + mount.Source, err = ctx.Expand(mount.Source) + if err != nil { + return mounts, errors.Wrapf(err, "expanding mount source %q", mount.Source) + } + + mount.Target, err = ctx.Expand(mount.Target) + if err != nil { + return mounts, errors.Wrapf(err, "expanding mount target %q", mount.Target) + } + + if mount.VolumeOptions != nil { + mount.VolumeOptions.Labels, err = expandMap(ctx, mount.VolumeOptions.Labels) + if err != nil { + return mounts, errors.Wrap(err, "expanding volume labels") + } + + if mount.VolumeOptions.DriverConfig != nil { + mount.VolumeOptions.DriverConfig.Options, err = expandMap(ctx, mount.VolumeOptions.DriverConfig.Options) + if err != nil { + return mounts, errors.Wrap(err, "expanding volume driver config") + } + } + } + + expanded[i] = mount + } + + return expanded, nil +} + +func expandMap(ctx Context, m map[string]string) (map[string]string, error) { + var ( + n = make(map[string]string, len(m)) + err error + ) + + for k, v := range m { + v, err = ctx.Expand(v) + if err != nil { + return m, errors.Wrapf(err, "expanding map entry %q=%q", k, v) + } + + n[k] = v + } + + return n, nil +} + +func expandEnv(ctx Context, values []string) ([]string, error) { + var result []string + for _, value := range values { + var ( + parts = strings.SplitN(value, "=", 2) + entry = parts[0] + ) + + if len(parts) > 1 { + expanded, err := ctx.Expand(parts[1]) + if err != nil { + return values, errors.Wrapf(err, "expanding env %q", value) + } + + entry = fmt.Sprintf("%s=%s", entry, expanded) + } + + result = append(result, entry) + } + + return result, nil +} diff --git a/vendor/github.com/docker/swarmkit/template/template.go b/vendor/github.com/docker/swarmkit/template/template.go new file mode 100644 index 0000000000..9f1517c662 --- /dev/null +++ b/vendor/github.com/docker/swarmkit/template/template.go @@ -0,0 +1,18 @@ +package template + +import ( + "strings" + "text/template" +) + +// funcMap defines functions for our template system. +var funcMap = template.FuncMap{ + "join": func(s ...string) string { + // first arg is sep, remaining args are strings to join + return strings.Join(s[1:], s[0]) + }, +} + +func newTemplate(s string) (*template.Template, error) { + return template.New("expansion").Option("missingkey=error").Funcs(funcMap).Parse(s) +}