template/context.go
package template
import (
"bytes"
"fmt"
"strings"
"text/template"
"github.com/moby/swarmkit/v2/agent/configs"
"github.com/moby/swarmkit/v2/agent/exec"
"github.com/moby/swarmkit/v2/agent/secrets"
"github.com/moby/swarmkit/v2/api"
"github.com/moby/swarmkit/v2/api/naming"
"github.com/pkg/errors"
)
// Platform holds information about the underlying platform of the node
type Platform struct {
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
// that have methods defined on them. The template would be able to
// invoke those methods.
type Context struct {
Service struct {
ID string
Name string
Labels map[string]string
}
Node struct {
ID string
Hostname string
Platform Platform
}
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!
}
}
// 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 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)
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, nil)
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
}
// PayloadContext provides a context for expanding a config or secret payload.
// NOTE: Be very careful adding any fields to this structure with types
// that have methods defined on them. The template would be able to
// invoke those methods.
type PayloadContext struct {
Context
t *api.Task
restrictedSecrets exec.SecretGetter
restrictedConfigs exec.ConfigGetter
sensitive bool
}
func (ctx *PayloadContext) secretGetter(target string) (string, error) {
if ctx.restrictedSecrets == nil {
return "", errors.New("secrets unavailable")
}
container := ctx.t.Spec.GetContainer()
if container == nil {
return "", errors.New("task is not a container")
}
for _, secretRef := range container.Secrets {
file := secretRef.GetFile()
if file != nil && file.Name == target {
secret, err := ctx.restrictedSecrets.Get(secretRef.SecretID)
if err != nil {
return "", err
}
ctx.sensitive = true
return string(secret.Spec.Data), nil
}
}
return "", errors.Errorf("secret target %s not found", target)
}
func (ctx *PayloadContext) configGetter(target string) (string, error) {
if ctx.restrictedConfigs == nil {
return "", errors.New("configs unavailable")
}
container := ctx.t.Spec.GetContainer()
if container == nil {
return "", errors.New("task is not a container")
}
for _, configRef := range container.Configs {
file := configRef.GetFile()
if file != nil && file.Name == target {
config, err := ctx.restrictedConfigs.Get(configRef.ConfigID)
if err != nil {
return "", err
}
return string(config.Spec.Data), nil
}
}
return "", errors.Errorf("config target %s not found", target)
}
func (ctx *PayloadContext) envGetter(variable string) (string, error) {
container := ctx.t.Spec.GetContainer()
if container == nil {
return "", errors.New("task is not a container")
}
for _, env := range container.Env {
parts := strings.SplitN(env, "=", 2)
if len(parts) > 1 && parts[0] == variable {
return parts[1], nil
}
}
return "", nil
}
// NewPayloadContextFromTask returns a new template context from the data
// 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(node *api.NodeDescription, t *api.Task, dependencies exec.DependencyGetter) (ctx PayloadContext) {
return PayloadContext{
Context: NewContext(node, t),
t: t,
restrictedSecrets: secrets.Restrict(dependencies.Secrets(), t),
restrictedConfigs: configs.Restrict(dependencies.Configs(), t),
}
}
// Expand treats the string s as a template and populates it with values from
// the context.
func (ctx *PayloadContext) Expand(s string) (string, error) {
funcMap := template.FuncMap{
"secret": ctx.secretGetter,
"config": ctx.configGetter,
"env": ctx.envGetter,
}
tmpl, err := newTemplate(s, funcMap)
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
}