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/agent/exec"
|
||||||
"github.com/docker/swarmkit/api"
|
"github.com/docker/swarmkit/api"
|
||||||
"github.com/docker/swarmkit/protobuf/ptypes"
|
"github.com/docker/swarmkit/protobuf/ptypes"
|
||||||
|
"github.com/docker/swarmkit/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -68,6 +69,17 @@ func (c *containerConfig) setTask(t *api.Task) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
c.task = t
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -118,6 +118,21 @@ func (s *DockerSwarmSuite) TestSwarmNodeListHostname(c *check.C) {
|
||||||
c.Assert(strings.Split(out, "\n")[0], checker.Contains, "HOSTNAME")
|
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
|
// Test case for #24270
|
||||||
func (s *DockerSwarmSuite) TestSwarmServiceListFilter(c *check.C) {
|
func (s *DockerSwarmSuite) TestSwarmServiceListFilter(c *check.C) {
|
||||||
d := s.AddDaemon(c, true, true)
|
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), "")
|
c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
|
||||||
|
|
||||||
out, err = d.Cmd("run", "-id", "--restart=always", "--net=foo", "--name=test", "busybox", "top")
|
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), "")
|
c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
|
||||||
|
|
||||||
out, err = d.Cmd("ps", "-q")
|
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), "")
|
c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
|
||||||
|
|
||||||
d.Restart()
|
d.Restart()
|
||||||
|
|
||||||
out, err = d.Cmd("ps", "-q")
|
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), "")
|
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)
|
d := s.AddDaemon(c, true, true)
|
||||||
|
|
||||||
out, err := d.Cmd("network", "create", "--attachable", "-d", "overlay", "foo")
|
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), "")
|
c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
|
||||||
|
|
||||||
_, err = d.Cmd("run", "-d", "--net=foo", "--name=first", "--net-alias=first-alias", "busybox", "top")
|
_, 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")
|
_, 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
|
// ping first container and its alias
|
||||||
_, err = d.Cmd("exec", "second", "ping", "-c", "1", "first")
|
_, 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")
|
_, 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) {
|
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/constraint"
|
||||||
"github.com/docker/swarmkit/manager/state/store"
|
"github.com/docker/swarmkit/manager/state/store"
|
||||||
"github.com/docker/swarmkit/protobuf/ptypes"
|
"github.com/docker/swarmkit/protobuf/ptypes"
|
||||||
|
"github.com/docker/swarmkit/template"
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/codes"
|
"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")
|
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
|
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…
Reference in a new issue