зеркало из https://github.com/microsoft/docker.git
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:
Родитель
6e885540a2
Коммит
6212ea669b
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
Загрузка…
Ссылка в новой задаче