Browse Source

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 8 years ago
parent
commit
6212ea669b

+ 12 - 0
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
 }
 

+ 23 - 8
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) {

+ 23 - 1
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
 	}
 

+ 72 - 0
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
+}

+ 118 - 0
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
+}

+ 18 - 0
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)
+}