mirror of
				https://github.com/moby/moby.git
				synced 2022-11-09 12:21:53 -05:00 
			
		
		
		
	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>
This commit is contained in:
		
							parent
							
								
									6e885540a2
								
							
						
					
					
						commit
						6212ea669b
					
				
					 6 changed files with 266 additions and 9 deletions
				
			
		| 
						 | 
				
			
			@ -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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										24
									
								
								vendor/github.com/docker/swarmkit/manager/controlapi/service.go
									
										
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										24
									
								
								vendor/github.com/docker/swarmkit/manager/controlapi/service.go
									
										
									
										generated
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -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
									
								
								vendor/github.com/docker/swarmkit/template/context.go
									
										
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								vendor/github.com/docker/swarmkit/template/context.go
									
										
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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
									
								
								vendor/github.com/docker/swarmkit/template/expand.go
									
										
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								vendor/github.com/docker/swarmkit/template/expand.go
									
										
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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
									
								
								vendor/github.com/docker/swarmkit/template/template.go
									
										
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								vendor/github.com/docker/swarmkit/template/template.go
									
										
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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)
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue