Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions agent/exec/dockerapi/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ type containerAdapter struct {
secrets exec.SecretGetter
}

func newContainerAdapter(client engineapi.APIClient, task *api.Task, secrets exec.SecretGetter) (*containerAdapter, error) {
ctnr, err := newContainerConfig(task)
func newContainerAdapter(client engineapi.APIClient, nodeDescription *api.NodeDescription, task *api.Task, secrets exec.SecretGetter) (*containerAdapter, error) {
ctnr, err := newContainerConfig(nodeDescription, task)
if err != nil {
return nil, err
}
Expand Down
8 changes: 4 additions & 4 deletions agent/exec/dockerapi/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ type containerConfig struct {

// newContainerConfig returns a validated container config. No methods should
// return an error if this function returns without error.
func newContainerConfig(t *api.Task) (*containerConfig, error) {
func newContainerConfig(n *api.NodeDescription, t *api.Task) (*containerConfig, error) {
var c containerConfig
return &c, c.setTask(t)
return &c, c.setTask(n, t)
}

func (c *containerConfig) setTask(t *api.Task) error {
func (c *containerConfig) setTask(n *api.NodeDescription, t *api.Task) error {
container := t.Spec.GetContainer()
if container == nil {
return exec.ErrRuntimeUnsupported
Expand All @@ -64,7 +64,7 @@ func (c *containerConfig) setTask(t *api.Task) error {
}

c.task = t
preparedSpec, err := template.ExpandContainerSpec(t)
preparedSpec, err := template.ExpandContainerSpec(n, t)
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions agent/exec/dockerapi/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ type controller struct {
var _ exec.Controller = &controller{}

// newController returns a docker exec controller for the provided task.
func newController(client engineapi.APIClient, task *api.Task, secrets exec.SecretGetter) (exec.Controller, error) {
adapter, err := newContainerAdapter(client, task, secrets)
func newController(client engineapi.APIClient, nodeDescription *api.NodeDescription, task *api.Task, secrets exec.SecretGetter) (exec.Controller, error) {
adapter, err := newContainerAdapter(client, nodeDescription, task, secrets)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion agent/exec/dockerapi/controller_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func TestControllerFlowIntegration(t *testing.T) {
return nil
})

ctlr, err := newController(client, task, nil)
ctlr, err := newController(client, nil, task, nil)
assert.NoError(t, err)
assert.NotNil(t, ctlr)
assert.NoError(t, ctlr.Prepare(ctx))
Expand Down
12 changes: 10 additions & 2 deletions agent/exec/dockerapi/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -411,11 +411,19 @@ func TestControllerRemove(t *testing.T) {
}

func genTestControllerEnv(t *testing.T, task *api.Task) (context.Context, *StubAPIClient, exec.Controller, *containerConfig, func()) {
testNodeDescription := &api.NodeDescription{
Hostname: "testHostname",
Platform: &api.Platform{
OS: "linux",
Architecture: "x86_64",
},
}

client := NewStubAPIClient()
ctlr, err := newController(client, task, nil)
ctlr, err := newController(client, testNodeDescription, task, nil)
assert.NoError(t, err)

config, err := newContainerConfig(task)
config, err := newContainerConfig(testNodeDescription, task)
assert.NoError(t, err)
assert.NotNil(t, config)

Expand Down
17 changes: 15 additions & 2 deletions agent/exec/dockerapi/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,25 @@ import (
"github.com/docker/swarmkit/api"
"github.com/docker/swarmkit/log"
"golang.org/x/net/context"
"sync"
)

type executor struct {
client engineapi.APIClient
secrets exec.SecretsManager
genericResources []*api.GenericResource
mutex sync.Mutex // This mutex protects the following node field
node *api.NodeDescription
}

// NewExecutor returns an executor from the docker client.
func NewExecutor(client engineapi.APIClient, genericResources []*api.GenericResource) exec.Executor {
return &executor{
var executor = &executor{
client: client,
secrets: secrets.NewManager(),
genericResources: genericResources,
}
return executor
}

// Describe returns the underlying node description from the docker client.
Expand Down Expand Up @@ -111,6 +115,11 @@ func (e *executor) Describe(ctx context.Context) (*api.NodeDescription, error) {
},
}

// Save the node information in the executor field
e.mutex.Lock()
e.node = description
e.mutex.Unlock()

return description, nil
}

Expand All @@ -120,7 +129,11 @@ func (e *executor) Configure(ctx context.Context, node *api.Node) error {

// Controller returns a docker container controller.
func (e *executor) Controller(t *api.Task) (exec.Controller, error) {
ctlr, err := newController(e.client, t, secrets.Restrict(e.secrets, t))
// Get the node description from the executor field
e.mutex.Lock()
nodeDescription := e.node
e.mutex.Unlock()
ctlr, err := newController(e.client, nodeDescription, t, secrets.Restrict(e.secrets, t))
if err != nil {
return nil, err
}
Expand Down
8 changes: 7 additions & 1 deletion manager/controlapi/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,13 @@ func validateContainerSpec(taskSpec api.TaskSpec) error {
// Building a empty/dummy Task to validate the templating and
// the resulting container spec as well. This is a *best effort*
// validation.
container, err := template.ExpandContainerSpec(&api.Task{
container, err := template.ExpandContainerSpec(&api.NodeDescription{
Hostname: "nodeHostname",
Platform: &api.Platform{
OS: "os",
Architecture: "architecture",
},
}, &api.Task{
Spec: taskSpec,
ServiceID: "serviceid",
Slot: 1,
Expand Down
32 changes: 25 additions & 7 deletions template/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ import (
"github.com/pkg/errors"
)

// Platform holds information about the underlying platform of the node
type Platform struct {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use api.Platform instead of redefining the struct here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forget I said that... realized this is going into a struct that will be accessable by templates, so no methods should be defined on the type.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's exactly what I thought 😉

Architecture string
OS string
}

// Context defines the strict set of values that can be injected into a
// template expression in SwarmKit data structure.
// NOTE: Be very careful adding any fields to this structure with types
Expand All @@ -27,7 +33,9 @@ type Context struct {
}

Node struct {
ID string
ID string
Hostname string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we're at it, would it make sense to add other properties from NodeDescription, like Architecture and OS?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Platform Platform
}

Task struct {
Expand All @@ -41,16 +49,25 @@ type Context struct {
}
}

// 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
// NewContext returns a new template context from the data available in the
// task and the node where it is scheduled to run.
// The provided context can then be used to populate runtime values in a
// ContainerSpec.
func NewContextFromTask(t *api.Task) (ctx Context) {
func NewContext(n *api.NodeDescription, 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

// Add node information to context only if we have them available
if n != nil {
ctx.Node.Hostname = n.Hostname
ctx.Node.Platform = Platform{
Architecture: n.Platform.Architecture,
OS: n.Platform.OS,
}
}
ctx.Task.ID = t.ID
ctx.Task.Name = naming.Task(t)

Expand Down Expand Up @@ -157,12 +174,13 @@ func (ctx PayloadContext) envGetter(variable string) (string, error) {
}

// NewPayloadContextFromTask returns a new template context from the data
// available in the task. This context also provides access to the configs
// available in the task and the node where it is scheduled to run.
// This context also provides access to the configs
// and secrets that the task has access to. The provided context can then
// be used to populate runtime values in a templated config or secret.
func NewPayloadContextFromTask(t *api.Task, dependencies exec.DependencyGetter) (ctx PayloadContext) {
func NewPayloadContextFromTask(node *api.NodeDescription, t *api.Task, dependencies exec.DependencyGetter) (ctx PayloadContext) {
return PayloadContext{
Context: NewContextFromTask(t),
Context: NewContext(node, t),
t: t,
restrictedSecrets: secrets.Restrict(dependencies.Secrets(), t),
restrictedConfigs: configs.Restrict(dependencies.Configs(), t),
Expand Down
75 changes: 69 additions & 6 deletions template/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import (

func TestTemplateContext(t *testing.T) {
for _, testcase := range []struct {
Test string
Task *api.Task
Context Context
Expected *api.ContainerSpec
Err error
Test string
Task *api.Task
Context Context
Expected *api.ContainerSpec
Err error
NodeDescription *api.NodeDescription
}{
{
Test: "Identity",
Expand All @@ -35,6 +36,8 @@ func TestTemplateContext(t *testing.T) {
},
}
}),
NodeDescription: modifyNode(func(n *api.NodeDescription) {
}),
Expected: &api.ContainerSpec{
Env: []string{
"NOTOUCH=dont",
Expand Down Expand Up @@ -70,6 +73,8 @@ func TestTemplateContext(t *testing.T) {
},
}
}),
NodeDescription: modifyNode(func(n *api.NodeDescription) {
}),
Expected: &api.ContainerSpec{
Labels: map[string]string{
"ContainerLabel": "should-NOT-end-up-as-task",
Expand Down Expand Up @@ -106,6 +111,8 @@ func TestTemplateContext(t *testing.T) {
},
}
}),
NodeDescription: modifyNode(func(n *api.NodeDescription) {
}),
Expected: &api.ContainerSpec{
Mounts: []api.Mount{
{
Expand All @@ -130,13 +137,53 @@ func TestTemplateContext(t *testing.T) {
},
}
}),
NodeDescription: modifyNode(func(n *api.NodeDescription) {
}),
Expected: &api.ContainerSpec{
Hostname: "myhost-10",
},
},
{
Test: "Node hostname",
Task: modifyTask(func(t *api.Task) {
t.Spec = api.TaskSpec{
Runtime: &api.TaskSpec_Container{
Container: &api.ContainerSpec{
Hostname: "myservice-{{.Node.Hostname}}",
},
},
}
}),
NodeDescription: modifyNode(func(n *api.NodeDescription) {
n.Hostname = "mynode"
}),
Expected: &api.ContainerSpec{
Hostname: "myservice-mynode",
},
},
{
Test: "Node architecture",
Task: modifyTask(func(t *api.Task) {
t.Spec = api.TaskSpec{
Runtime: &api.TaskSpec_Container{
Container: &api.ContainerSpec{
Hostname: "{{.Node.Hostname}}-{{.Node.Platform.OS}}-{{.Node.Platform.Architecture}}",
},
},
}
}),
NodeDescription: modifyNode(func(n *api.NodeDescription) {
n.Hostname = "mynode"
n.Platform.Architecture = "myarchitecture"
n.Platform.OS = "myos"
}),
Expected: &api.ContainerSpec{
Hostname: "mynode-myos-myarchitecture",
},
},
} {
t.Run(testcase.Test, func(t *testing.T) {
spec, err := ExpandContainerSpec(testcase.Task)
spec, err := ExpandContainerSpec(testcase.NodeDescription, testcase.Task)
if err != nil {
if testcase.Err == nil {
t.Fatalf("unexpected error: %v", err)
Expand Down Expand Up @@ -194,6 +241,22 @@ func modifyTask(fn func(t *api.Task)) *api.Task {
return t
}

// modifyNode generates a node with interesting values then calls the function
// with it. The caller can then modify the node and return the result.
func modifyNode(fn func(n *api.NodeDescription)) *api.NodeDescription {
n := &api.NodeDescription{
Hostname: "nodeHostname",
Platform: &api.Platform{
Architecture: "x86_64",
OS: "linux",
},
}

fn(n)

return n
}

// visitAllTemplatedFields does just that.
// TODO(stevvooe): Might be best to make this the actual implementation.
func visitAllTemplatedFields(spec *api.ContainerSpec, fn func(value string)) {
Expand Down
15 changes: 8 additions & 7 deletions template/expand.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,19 @@ import (
)

// ExpandContainerSpec expands templated fields in the runtime using the task
// state. Templating is all evaluated on the agent-side, before execution.
// state and the node where it is scheduled to run.
// 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) {
func ExpandContainerSpec(n *api.NodeDescription, 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)
ctx := NewContext(n, t)

var err error
container.Env, err = expandEnv(ctx, container.Env)
Expand Down Expand Up @@ -128,12 +129,12 @@ func expandPayload(ctx PayloadContext, payload []byte) ([]byte, error) {

// ExpandSecretSpec expands the template inside the secret payload, if any.
// Templating is evaluated on the agent-side.
func ExpandSecretSpec(s *api.Secret, t *api.Task, dependencies exec.DependencyGetter) (*api.SecretSpec, error) {
func ExpandSecretSpec(s *api.Secret, node *api.NodeDescription, t *api.Task, dependencies exec.DependencyGetter) (*api.SecretSpec, error) {
if s.Spec.Templating == nil {
return &s.Spec, nil
}
if s.Spec.Templating.Name == "golang" {
ctx := NewPayloadContextFromTask(t, dependencies)
ctx := NewPayloadContextFromTask(node, t, dependencies)
secretSpec := s.Spec.Copy()

var err error
Expand All @@ -145,12 +146,12 @@ func ExpandSecretSpec(s *api.Secret, t *api.Task, dependencies exec.DependencyGe

// ExpandConfigSpec expands the template inside the config payload, if any.
// Templating is evaluated on the agent-side.
func ExpandConfigSpec(c *api.Config, t *api.Task, dependencies exec.DependencyGetter) (*api.ConfigSpec, error) {
func ExpandConfigSpec(c *api.Config, node *api.NodeDescription, t *api.Task, dependencies exec.DependencyGetter) (*api.ConfigSpec, error) {
if c.Spec.Templating == nil {
return &c.Spec, nil
}
if c.Spec.Templating.Name == "golang" {
ctx := NewPayloadContextFromTask(t, dependencies)
ctx := NewPayloadContextFromTask(node, t, dependencies)
configSpec := c.Spec.Copy()

var err error
Expand Down
Loading